Skip to content

Commit 1d65da8

Browse files
authored
Merge pull request #209 from mglaman/190-drupal-service-return-type
Return type extension for \Drupal::service calls
2 parents 8709438 + ec08902 commit 1d65da8

File tree

6 files changed

+167
-13
lines changed

6 files changed

+167
-13
lines changed

extension.neon

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ services:
3838
entityTypeStorageMapping: %drupal.entityTypeStorageMapping%
3939
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
4040
-
41-
class: PHPStan\Type\ServiceDynamicReturnTypeExtension
41+
class: PHPStan\Type\ContainerDynamicReturnTypeExtension
4242
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
43+
-
44+
class: PHPStan\Type\DrupalServiceDynamicReturnTypeExtension
45+
tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension]
4346
-
4447
class: PHPStan\Reflection\EntityFieldsViaMagicReflectionExtension
4548
tags: [phpstan.broker.propertiesClassReflectionExtension]
@@ -69,3 +72,6 @@ services:
6972
-
7073
class: PHPStan\Rules\Deprecations\GetDeprecatedServiceRule
7174
tags: [phpstan.rules.rule]
75+
-
76+
class: PHPStan\Rules\Deprecations\StaticServiceDeprecatedServiceRule
77+
tags: [phpstan.rules.rule]
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Deprecations;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Drupal\DrupalServiceDefinition;
8+
use PHPStan\Drupal\ServiceMap;
9+
use PHPStan\Rules\Rule;
10+
11+
final class StaticServiceDeprecatedServiceRule implements Rule
12+
{
13+
14+
/**
15+
* @var ServiceMap
16+
*/
17+
private $serviceMap;
18+
19+
public function __construct(ServiceMap $serviceMap)
20+
{
21+
$this->serviceMap = $serviceMap;
22+
}
23+
24+
public function getNodeType(): string
25+
{
26+
return Node\Expr\StaticCall::class;
27+
}
28+
29+
public function processNode(Node $node, Scope $scope): array
30+
{
31+
assert($node instanceof Node\Expr\StaticCall);
32+
if (!$node->name instanceof Node\Identifier) {
33+
return [];
34+
}
35+
$method_name = $node->name->toString();
36+
if ($method_name !== 'service') {
37+
return [];
38+
}
39+
40+
$class = $node->class;
41+
if ($class instanceof Node\Name) {
42+
$calledOnType = $scope->resolveTypeByName($class);
43+
} else {
44+
$calledOnType = $scope->getType($class);
45+
}
46+
$methodReflection = $scope->getMethodReflection($calledOnType, $node->name->toString());
47+
48+
if ($methodReflection === null) {
49+
return [];
50+
}
51+
$declaringClass = $methodReflection->getDeclaringClass();
52+
if ($declaringClass->getName() !== 'Drupal') {
53+
return [];
54+
}
55+
56+
$serviceNameArg = $node->args[0];
57+
assert($serviceNameArg instanceof Node\Arg);
58+
$serviceName = $serviceNameArg->value;
59+
// @todo check if var, otherwise throw.
60+
// ACTUALLY what if it was a constant? can we use a resolver.
61+
if (!$serviceName instanceof Node\Scalar\String_) {
62+
return [];
63+
}
64+
65+
$service = $this->serviceMap->getService($serviceName->value);
66+
if (($service instanceof DrupalServiceDefinition) && $service->isDeprecated()) {
67+
return [$service->getDeprecatedDescription()];
68+
}
69+
70+
return [];
71+
}
72+
}

src/Type/ServiceDynamicReturnTypeExtension.php renamed to src/Type/ContainerDynamicReturnTypeExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use PHPStan\Type\Constant\ConstantBooleanType;
1515
use Psr\Container\ContainerInterface;
1616

17-
class ServiceDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
17+
class ContainerDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
1818
{
1919
/**
2020
* @var ServiceMap
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PhpParser\Node\Expr\StaticCall;
6+
use PhpParser\Node\Scalar\String_;
7+
use PhpParser\Node\VariadicPlaceholder;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Drupal\DrupalServiceDefinition;
10+
use PHPStan\Drupal\ServiceMap;
11+
use PHPStan\Reflection\MethodReflection;
12+
use PHPStan\Reflection\ParametersAcceptorSelector;
13+
use PHPStan\ShouldNotHappenException;
14+
15+
class DrupalServiceDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
16+
{
17+
/**
18+
* @var ServiceMap
19+
*/
20+
private $serviceMap;
21+
22+
public function __construct(ServiceMap $serviceMap)
23+
{
24+
$this->serviceMap = $serviceMap;
25+
}
26+
27+
public function getClass(): string
28+
{
29+
return \Drupal::class;
30+
}
31+
32+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
33+
{
34+
return $methodReflection->getName() === 'service';
35+
}
36+
37+
public function getTypeFromStaticMethodCall(
38+
MethodReflection $methodReflection,
39+
StaticCall $methodCall,
40+
Scope $scope
41+
): Type {
42+
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
43+
if (!isset($methodCall->args[0])) {
44+
return $returnType;
45+
}
46+
47+
$arg1 = $methodCall->args[0];
48+
if ($arg1 instanceof VariadicPlaceholder) {
49+
throw new ShouldNotHappenException();
50+
}
51+
$arg1 = $arg1->value;
52+
if (!$arg1 instanceof String_) {
53+
// @todo determine what these types are.
54+
return $returnType;
55+
}
56+
57+
$serviceId = $arg1->value;
58+
$service = $this->serviceMap->getService($serviceId);
59+
if ($service instanceof DrupalServiceDefinition) {
60+
// Work around Drupal misusing the SplString class for string
61+
// pseudo-services such as 'app.root'.
62+
// @see https://www.drupal.org/project/drupal/issues/3074585
63+
if ($service->getClass() === 'SplString') {
64+
return new StringType();
65+
}
66+
return new ObjectType($service->getClass() ?? $serviceId);
67+
}
68+
return $returnType;
69+
}
70+
}

tests/fixtures/drupal/modules/phpstan_fixtures/src/TestServicesMappingExtension.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
namespace Drupal\phpstan_fixtures;
44

5-
use Drupal\Core\Entity\EntityManager;
6-
75
class TestServicesMappingExtension {
8-
public function test() {
6+
public function testEntityManager() {
97
$entity_manager = \Drupal::getContainer()->get('entity.manager');
108
$doesNotExist = $entity_manager->thisMethodDoesNotExist();
119
$definitions = $entity_manager->getDefinitions();
1210
}
11+
12+
public function testPathAliasManagerServiceRename() {
13+
$manager = \Drupal::service('path.alias_manager');
14+
$path = $manager->getPathByAlias('/foo/bar', 'en');
15+
}
1316
}

tests/src/DrupalIntegrationTest.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,30 +91,33 @@ public function testServiceMapping8()
9191
'Call to deprecated method getDefinitions() of class Drupal\\Core\\Entity\\EntityManager:
9292
in drupal:8.0.0 and is removed from drupal:9.0.0.
9393
Use \\Drupal\\Core\\Entity\\EntityTypeManagerInterface::getDefinitions()
94-
instead.'
94+
instead.',
95+
'The "path.alias_manager" service is deprecated. Use "path_alias.manager" instead. See https://drupal.org/node/3092086',
96+
'\Drupal calls should be avoided in classes, use dependency injection instead',
9597
];
9698
$errors = $this->runAnalyze(__DIR__ . '/../fixtures/drupal/modules/phpstan_fixtures/src/TestServicesMappingExtension.php');
97-
$this->assertCount(4, $errors->getErrors());
98-
$this->assertCount(0, $errors->getInternalErrors());
99+
self::assertCount(count($errorMessages), $errors->getErrors());
100+
self::assertCount(0, $errors->getInternalErrors());
99101
foreach ($errors->getErrors() as $key => $error) {
100-
$this->assertEquals($errorMessages[$key], $error->getMessage());
102+
self::assertEquals($errorMessages[$key], $error->getMessage());
101103
}
102104
}
103105

104106
public function testServiceMapping9()
105107
{
106108
if (version_compare('9.0.0', \Drupal::VERSION) === 1) {
107-
$this->markTestSkipped('Only tested on Drupal 9.x.x');
109+
self::markTestSkipped('Only tested on Drupal 9.x.x');
108110
}
109111
// @todo: the actual error should be the fact `entity.manager` does not exist.
110112
$errorMessages = [
111113
'\Drupal calls should be avoided in classes, use dependency injection instead',
114+
'\Drupal calls should be avoided in classes, use dependency injection instead',
112115
];
113116
$errors = $this->runAnalyze(__DIR__ . '/../fixtures/drupal/modules/phpstan_fixtures/src/TestServicesMappingExtension.php');
114-
$this->assertCount(1, $errors->getErrors());
115-
$this->assertCount(0, $errors->getInternalErrors());
117+
self::assertCount(count($errorMessages), $errors->getErrors());
118+
self::assertCount(0, $errors->getInternalErrors());
116119
foreach ($errors->getErrors() as $key => $error) {
117-
$this->assertEquals($errorMessages[$key], $error->getMessage());
120+
self::assertEquals($errorMessages[$key], $error->getMessage());
118121
}
119122
}
120123

0 commit comments

Comments
 (0)