Skip to content

Commit 57f8fcb

Browse files
authored
Merge pull request #7 from lookyman/class-const-fetch-support
Support for ClassConstFetch
2 parents d2ee174 + f95b96a commit 57f8fcb

6 files changed

+81
-73
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ parameters:
2525

2626
## Limitations
2727

28-
It can only recognize pure strings passed into `ContainerInterface::get()` method. This follows from the nature of static code analysis.
28+
It can only recognize pure strings or `::class` constants passed into `ContainerInterface::get()` method. This follows from the nature of static code analysis.
2929

3030
You have to provide a path to `srcDevDebugProjectContainer.xml` or similar xml file describing your container.

src/Rules/ContainerInterfacePrivateServiceRule.php

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
use PhpParser\Node;
1212
use PhpParser\Node\Arg;
1313
use PhpParser\Node\Expr\MethodCall;
14-
use PhpParser\Node\Scalar\String_;
1514
use Symfony\Component\DependencyInjection\ContainerInterface;
1615

1716
final class ContainerInterfacePrivateServiceRule implements Rule
@@ -34,20 +33,18 @@ public function getNodeType(): string
3433

3534
public function processNode(Node $node, Scope $scope): array
3635
{
37-
$services = $this->serviceMap->getServices();
3836
if ($node instanceof MethodCall && $node->name === 'get') {
3937
$type = $scope->getType($node->var);
40-
if (!$type instanceof ObjectType) {
41-
return [];
42-
}
43-
return $type->getClassName() === ContainerInterface::class
38+
if ($type instanceof ObjectType
39+
&& $type->getClassName() === ContainerInterface::class
4440
&& isset($node->args[0])
4541
&& $node->args[0] instanceof Arg
46-
&& $node->args[0]->value instanceof String_
47-
&& \array_key_exists($node->args[0]->value->value, $services)
48-
&& !$services[$node->args[0]->value->value]['public']
49-
? [\sprintf('Service "%s" is private.', $node->args[0]->value->value)]
50-
: [];
42+
) {
43+
$service = $this->serviceMap->getServiceFromNode($node->args[0]->value);
44+
if ($service !== \null && !$service['public']) {
45+
return [\sprintf('Service "%s" is private.', $service['id'])];
46+
}
47+
}
5148
}
5249
return [];
5350
}

src/Rules/ContainerInterfaceUnknownServiceRule.php

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
use PhpParser\Node;
1212
use PhpParser\Node\Arg;
1313
use PhpParser\Node\Expr\MethodCall;
14-
use PhpParser\Node\Scalar\String_;
1514
use Symfony\Component\DependencyInjection\ContainerInterface;
1615

1716
final class ContainerInterfaceUnknownServiceRule implements Rule
@@ -34,19 +33,18 @@ public function getNodeType(): string
3433

3534
public function processNode(Node $node, Scope $scope): array
3635
{
37-
$services = $this->serviceMap->getServices();
3836
if ($node instanceof MethodCall && $node->name === 'get') {
3937
$type = $scope->getType($node->var);
40-
if (!$type instanceof ObjectType) {
41-
return [];
42-
}
43-
return $type->getClassName() === ContainerInterface::class
38+
if ($type instanceof ObjectType
39+
&& $type->getClassName() === ContainerInterface::class
4440
&& isset($node->args[0])
4541
&& $node->args[0] instanceof Arg
46-
&& $node->args[0]->value instanceof String_
47-
&& !\array_key_exists($node->args[0]->value->value, $services)
48-
? [\sprintf('Service "%s" is not registered in the container.', $node->args[0]->value->value)]
49-
: [];
42+
) {
43+
$service = $this->serviceMap->getServiceFromNode($node->args[0]->value);
44+
if ($service === \null) {
45+
return [\sprintf('Service "%s" is not registered in the container.', ServiceMap::getServiceIdFromNode($node->args[0]->value))];
46+
}
47+
}
5048
}
5149
return [];
5250
}

src/ServiceMap.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
namespace Lookyman\PHPStan\Symfony;
66

7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\ClassConstFetch;
9+
use PhpParser\Node\Name;
10+
use PhpParser\Node\Scalar\String_;
11+
712
final class ServiceMap
813
{
914

@@ -22,6 +27,7 @@ public function __construct(string $containerXml)
2227
continue;
2328
}
2429
$service = [
30+
'id' => (string) $attrs->id,
2531
'class' => isset($attrs->class) ? (string) $attrs->class : \null,
2632
'public' => !isset($attrs->public) || (string) $attrs->public !== 'false',
2733
'synthetic' => isset($attrs->synthetic) && (string) $attrs->synthetic === 'true',
@@ -35,6 +41,7 @@ public function __construct(string $containerXml)
3541
foreach ($aliases as $id => $alias) {
3642
if (\array_key_exists($alias['alias'], $this->services)) {
3743
$this->services[$id] = [
44+
'id' => $id,
3845
'class' => $this->services[$alias['alias']]['class'],
3946
'public' => $alias['public'],
4047
'synthetic' => $alias['synthetic'],
@@ -43,9 +50,29 @@ public function __construct(string $containerXml)
4350
}
4451
}
4552

46-
public function getServices(): array
53+
/**
54+
* @param Node $node
55+
* @return array|null
56+
*/
57+
public function getServiceFromNode(Node $node)
58+
{
59+
$serviceId = self::getServiceIdFromNode($node);
60+
return $serviceId !== \null && \array_key_exists($serviceId, $this->services) ? $this->services[$serviceId] : \null;
61+
}
62+
63+
/**
64+
* @param Node $node
65+
* @return string|null
66+
*/
67+
public static function getServiceIdFromNode(Node $node)
4768
{
48-
return $this->services;
69+
if ($node instanceof String_) {
70+
return $node->value;
71+
}
72+
if ($node instanceof ClassConstFetch && $node->class instanceof Name) {
73+
return $node->class->toString();
74+
}
75+
return \null;
4976
}
5077

5178
}

src/Type/ContainerInterfaceDynamicReturnTypeExtension.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use PHPStan\Type\Type;
1313
use PhpParser\Node\Arg;
1414
use PhpParser\Node\Expr\MethodCall;
15-
use PhpParser\Node\Scalar\String_;
1615
use Symfony\Component\DependencyInjection\ContainerInterface;
1716

1817
final class ContainerInterfaceDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
@@ -43,14 +42,15 @@ public function getTypeFromMethodCall(
4342
MethodCall $methodCall,
4443
Scope $scope
4544
): Type {
46-
$services = $this->serviceMap->getServices();
47-
return isset($methodCall->args[0])
45+
if (isset($methodCall->args[0])
4846
&& $methodCall->args[0] instanceof Arg
49-
&& $methodCall->args[0]->value instanceof String_
50-
&& \array_key_exists($methodCall->args[0]->value->value, $services)
51-
&& !$services[$methodCall->args[0]->value->value]['synthetic']
52-
? new ObjectType($services[$methodCall->args[0]->value->value]['class'])
53-
: $methodReflection->getReturnType();
47+
) {
48+
$service = $this->serviceMap->getServiceFromNode($methodCall->args[0]->value);
49+
if ($service !== \null && !$service['synthetic']) {
50+
return new ObjectType($service['class'] ?? $service['id']);
51+
}
52+
}
53+
return $methodReflection->getReturnType();
5454
}
5555

5656
}

tests/ServiceMapTest.php

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,56 +5,42 @@
55
namespace Lookyman\PHPStan\Symfony;
66

77
use PHPUnit\Framework\TestCase;
8+
use PhpParser\Node\Expr\ClassConstFetch;
9+
use PhpParser\Node\Name;
10+
use PhpParser\Node\Scalar\String_;
811

912
/**
1013
* @covers \Lookyman\PHPStan\Symfony\ServiceMap
1114
*/
1215
final class ServiceMapTest extends TestCase
1316
{
1417

15-
public function testGetServices()
18+
/**
19+
* @dataProvider getServiceFromNodeProvider
20+
*/
21+
public function testGetServiceFromNode(array $service)
1622
{
1723
$serviceMap = new ServiceMap(__DIR__ . '/container.xml');
18-
self::assertEquals(
19-
[
20-
'withoutClass' => [
21-
'class' => \null,
22-
'public' => \true,
23-
'synthetic' => \false,
24-
],
25-
'withClass' => [
26-
'class' => 'Foo',
27-
'public' => \true,
28-
'synthetic' => \false,
29-
],
30-
'withoutPublic' => [
31-
'class' => 'Foo',
32-
'public' => \true,
33-
'synthetic' => \false,
34-
],
35-
'publicNotFalse' => [
36-
'class' => 'Foo',
37-
'public' => \true,
38-
'synthetic' => \false,
39-
],
40-
'private' => [
41-
'class' => 'Foo',
42-
'public' => \false,
43-
'synthetic' => \false,
44-
],
45-
'synthetic' => [
46-
'class' => 'Foo',
47-
'public' => \true,
48-
'synthetic' => \true,
49-
],
50-
'alias' => [
51-
'class' => 'Foo',
52-
'public' => \true,
53-
'synthetic' => \false,
54-
],
55-
],
56-
$serviceMap->getServices()
57-
);
24+
self::assertEquals($service, $serviceMap->getServiceFromNode(new String_($service['id'])));
25+
}
26+
27+
public function getServiceFromNodeProvider(): array
28+
{
29+
return [
30+
[['id' => 'withoutClass', 'class' => \null, 'public' => \true, 'synthetic' => \false]],
31+
[['id' => 'withClass', 'class' => 'Foo', 'public' => \true, 'synthetic' => \false]],
32+
[['id' => 'withoutPublic', 'class' => 'Foo', 'public' => \true, 'synthetic' => \false]],
33+
[['id' => 'publicNotFalse', 'class' => 'Foo', 'public' => \true, 'synthetic' => \false]],
34+
[['id' => 'private', 'class' => 'Foo', 'public' => \false, 'synthetic' => \false]],
35+
[['id' => 'synthetic', 'class' => 'Foo', 'public' => \true, 'synthetic' => \true]],
36+
[['id' => 'alias', 'class' => 'Foo', 'public' => \true, 'synthetic' => \false]],
37+
];
38+
}
39+
40+
public function testGetServiceIdFromNode()
41+
{
42+
self::assertEquals('foo', ServiceMap::getServiceIdFromNode(new String_('foo')));
43+
self::assertEquals('bar', ServiceMap::getServiceIdFromNode(new ClassConstFetch(new Name('bar'), '')));
5844
}
5945

6046
}

0 commit comments

Comments
 (0)