Skip to content

Commit e4aee72

Browse files
authored
Merge pull request #206 from mglaman/container-get-deprecation-rule
Allow service definitions to be identified as deprecated
2 parents 515749e + 6c85429 commit e4aee72

File tree

10 files changed

+230
-17
lines changed

10 files changed

+230
-17
lines changed

.github/workflows/php.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
- name: "Install dependencies"
4242
run: "composer update --no-progress --prefer-dist"
4343
- name: "Downgrade dev dependencies"
44-
run: "composer require phpunit/phpunit:6.5.14 drush/drush:~9 drupal/core-recommended:${{ matrix.drupal }} drupal/core-dev:${{ matrix.drupal }} --with-all-dependencies"
44+
run: "composer require phpunit/phpunit:6.5.14 drush/drush:~9 drupal/core-recommended:${{ matrix.drupal }} drupal/core-dev:${{ matrix.drupal }} --with-all-dependencies --dev"
4545
if: ${{ matrix.drupal == '^8.9' }}
4646
- name: "Add phpspec/prophecy-phpunit"
4747
run: "composer require --dev phpspec/prophecy-phpunit:^2"

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,7 @@ services:
6565
-
6666
class: PHPStan\Rules\Drupal\Tests\BrowserTestBaseDefaultThemeRule
6767
tags: [phpstan.rules.rule]
68+
69+
-
70+
class: PHPStan\Rules\Deprecations\GetDeprecatedServiceRule
71+
tags: [phpstan.rules.rule]

src/Drupal/DrupalServiceDefinition.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,21 @@ class DrupalServiceDefinition
2020
*/
2121
private $public;
2222

23+
/**
24+
* @var bool
25+
*/
26+
private $deprecated = false;
27+
28+
/**
29+
* @var string|null
30+
*/
31+
private $deprecationTemplate;
32+
33+
/**
34+
* @var string
35+
*/
36+
private static $defaultDeprecationTemplate = 'The "%service_id%" service is deprecated. You should stop using it, as it will soon be removed.';
37+
2338
/**
2439
* @var string|null
2540
*/
@@ -33,6 +48,12 @@ public function __construct(string $id, ?string $class, bool $public = true, ?st
3348
$this->alias = $alias;
3449
}
3550

51+
public function setDeprecated(bool $status = true, ?string $template = null): void
52+
{
53+
$this->deprecated = $status;
54+
$this->deprecationTemplate = $template;
55+
}
56+
3657
/**
3758
* @return string
3859
*/
@@ -64,4 +85,14 @@ public function getAlias(): ?string
6485
{
6586
return $this->alias;
6687
}
88+
89+
public function isDeprecated(): bool
90+
{
91+
return $this->deprecated;
92+
}
93+
94+
public function getDeprecatedDescription(): string
95+
{
96+
return str_replace('%service_id%', $this->id, $this->deprecationTemplate ?? self::$defaultDeprecationTemplate);
97+
}
6798
}

src/Drupal/ServiceMap.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ public function setDrupalServices(array $drupalServices): void
4949
$serviceDefinition['public'] ?? true,
5050
$serviceDefinition['alias'] ?? null
5151
);
52+
$deprecated = $serviceDefinition['deprecated'] ?? null;
53+
if ($deprecated) {
54+
$this->services[$serviceId]->setDeprecated(true, $deprecated);
55+
}
5256
}
5357
}
5458
}

src/Rules/Deprecations/AccessDeprecatedConstant.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Broker\Broker;
8-
use PHPStan\Reflection\DeprecatableReflection;
98
use PHPStan\Reflection\FunctionReflection;
109

1110
class AccessDeprecatedConstant implements \PHPStan\Rules\Rule
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 GetDeprecatedServiceRule 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\MethodCall::class;
27+
}
28+
29+
public function processNode(Node $node, Scope $scope): array
30+
{
31+
assert($node instanceof Node\Expr\MethodCall);
32+
if (!$node->name instanceof Node\Identifier) {
33+
return [];
34+
}
35+
$method_name = $node->name->toString();
36+
if ($method_name !== 'get') {
37+
return [];
38+
}
39+
$methodReflection = $scope->getMethodReflection($scope->getType($node->var), $node->name->toString());
40+
if ($methodReflection === null) {
41+
return [];
42+
}
43+
$declaringClass = $methodReflection->getDeclaringClass();
44+
if ($declaringClass->getName() !== 'Symfony\Component\DependencyInjection\ContainerInterface') {
45+
return [];
46+
}
47+
$serviceNameArg = $node->args[0];
48+
assert($serviceNameArg instanceof Node\Arg);
49+
$serviceName = $serviceNameArg->value;
50+
// @todo check if var, otherwise throw.
51+
// ACTUALLY what if it was a constant? can we use a resolver.
52+
if (!$serviceName instanceof Node\Scalar\String_) {
53+
return [];
54+
}
55+
56+
$service = $this->serviceMap->getService($serviceName->value);
57+
if (($service instanceof DrupalServiceDefinition) && $service->isDeprecated()) {
58+
return [$service->getDeprecatedDescription()];
59+
}
60+
61+
return [];
62+
}
63+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace Drupal\phpstan_fixtures\Controller;
4+
5+
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
6+
use Drupal\Core\Entity\EntityManagerInterface;
7+
use Symfony\Component\DependencyInjection\ContainerInterface;
8+
9+
class EntityManagerInjectedController implements ContainerInjectionInterface {
10+
11+
/**
12+
* @var EntityManagerInterface
13+
*/
14+
protected $entityManager;
15+
16+
public function __construct(EntityManagerInterface $entity_manager)
17+
{
18+
$this->entityManager = $entity_manager;
19+
}
20+
21+
public static function create(ContainerInterface $container)
22+
{
23+
return new self(
24+
$container->get('entity.manager')
25+
);
26+
}
27+
28+
public function handle() {
29+
$storage = $this->entityManager->getStorage('node');
30+
return [];
31+
}
32+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace Drupal\phpstan_fixtures\Plugin\Block;
4+
5+
use Drupal\Core\Block\BlockBase;
6+
use Drupal\Core\Entity\EntityManagerInterface;
7+
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
8+
use Symfony\Component\DependencyInjection\ContainerInterface;
9+
10+
/**
11+
* Block with entity.manager injected.
12+
*
13+
* @Block(
14+
* id = "entity_manager_block",
15+
* )
16+
*/
17+
class EntityManagerInjectedBlock extends BlockBase implements ContainerFactoryPluginInterface {
18+
19+
/**
20+
* @var EntityManagerInterface
21+
*/
22+
protected $entityManager;
23+
24+
public function __construct($configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager) {
25+
parent::__construct($configuration, $plugin_id, $plugin_definition);
26+
$this->entityManager = $entity_manager;
27+
}
28+
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
33+
return new static(
34+
$configuration,
35+
$plugin_id,
36+
$plugin_definition,
37+
$container->get('entity.manager')
38+
);
39+
}
40+
41+
public function build()
42+
{
43+
$storage = $this->entityManager->getStorage('node');
44+
return [];
45+
}
46+
}

tests/src/DeprecationRulesTest.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ class DeprecationRulesTest extends AnalyzerTestBase
88
/**
99
* @dataProvider dataDeprecatedSamples
1010
*/
11-
public function testDeprecationRules(string $path, int $count, array $errorMessages)
11+
public function testDeprecationRules(string $path, int $count, array $errorMessages): void
1212
{
1313
if (version_compare('9.0.0', \Drupal::VERSION) !== 1) {
1414
$this->markTestSkipped('Only tested on Drupal 8.x.x');
1515
}
1616
$errors = $this->runAnalyze($path);
17-
$this->assertCount($count, $errors->getErrors(), var_export($errors, true));
17+
self::assertCount($count, $errors->getErrors(), var_export($errors, true));
1818
foreach ($errors->getErrors() as $key => $error) {
19-
$this->assertEquals($errorMessages[$key], $error->getMessage());
19+
self::assertEquals($errorMessages[$key], $error->getMessage());
2020
}
2121
}
2222

@@ -50,5 +50,24 @@ public function dataDeprecatedSamples(): \Generator
5050
'Call to deprecated constant DATETIME_DATE_STORAGE_FORMAT: Deprecated in drupal:8.5.0 and is removed from drupal:9.0.0. Use \Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::DATE_STORAGE_FORMAT instead.',
5151
]
5252
];
53+
yield 'entity.manager ContainerInjectionInterface test' => [
54+
__DIR__ . '/../fixtures/drupal/modules/phpstan_fixtures/src/Controller/EntityManagerInjectedController.php',
55+
2,
56+
[
57+
'Parameter $entity_manager of method Drupal\\phpstan_fixtures\\Controller\\EntityManagerInjectedController::__construct() has typehint with deprecated interface Drupal\\Core\\Entity\\EntityManagerInterface:
58+
in drupal:8.0.0 and is removed from drupal:9.0.0.',
59+
'The "entity.manager" service is deprecated. You should use the \'entity_type.manager\' service instead.'
60+
]
61+
];
62+
yield 'entity.manager ContainerFactoryPluginInterface test' => [
63+
__DIR__ . '/../fixtures/drupal/modules/phpstan_fixtures/src/Plugin/Block/EntityManagerInjectedBlock.php',
64+
3,
65+
[
66+
'Parameter $entity_manager of method Drupal\\phpstan_fixtures\\Plugin\\Block\\EntityManagerInjectedBlock::__construct() has typehint with deprecated interface Drupal\\Core\\Entity\\EntityManagerInterface:
67+
in drupal:8.0.0 and is removed from drupal:9.0.0.',
68+
'Unsafe usage of new static().',
69+
'The "entity.manager" service is deprecated. You should use the \'entity_type.manager\' service instead.'
70+
]
71+
];
5372
}
5473
}

tests/src/DrupalIntegrationTest.php

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,27 @@ public function testDrupalTestInChildSiteContant() {
3939
$this->assertCount(0, $errors->getInternalErrors());
4040
}
4141

42-
public function testExtensionReportsError() {
42+
public function testExtensionReportsError(): void
43+
{
4344
$is_d9 = version_compare('9.0.0', \Drupal::VERSION) !== 1;
4445
$errors = $this->runAnalyze(__DIR__ . '/../fixtures/drupal/modules/phpstan_fixtures/phpstan_fixtures.module');
45-
$assert_count = ($is_d9) ? 4 : 3;
46-
$this->assertCount($assert_count, $errors->getErrors(), var_export($errors, true));
47-
$this->assertCount(0, $errors->getInternalErrors(), var_export($errors, true));
46+
$assert_count = ($is_d9) ? 5 : 3;
47+
self::assertCount($assert_count, $errors->getErrors(), var_export($errors, true));
48+
self::assertCount(0, $errors->getInternalErrors(), var_export($errors, true));
4849

4950
$errors = $errors->getErrors();
5051
$error = array_shift($errors);
51-
$this->assertEquals('If condition is always false.', $error->getMessage());
52+
self::assertEquals('If condition is always false.', $error->getMessage());
5253
$error = array_shift($errors);
53-
$this->assertEquals('Function phpstan_fixtures_MissingReturnRule() should return string but return statement is missing.', $error->getMessage());
54+
self::assertEquals('Function phpstan_fixtures_MissingReturnRule() should return string but return statement is missing.', $error->getMessage());
5455
if ($is_d9) {
5556
$error = array_shift($errors);
56-
$this->assertEquals('Binary operation "." between SplString and \'/core/includes…\' results in an error.', $error->getMessage());
57+
self::assertEquals('The "app.root" service is deprecated in drupal:9.0.0 and is removed from drupal:10.0.0. Use the app.root parameter instead. See https://www.drupal.org/node/3080612', $error->getMessage());
58+
$error = array_shift($errors);
59+
self::assertEquals('Binary operation "." between SplString and \'/core/includes…\' results in an error.', $error->getMessage());
5760
}
5861
$error = array_shift($errors);
59-
$this->assertNotFalse(strpos($error->getMessage(), 'phpstan_fixtures/phpstan_fixtures.fetch.inc could not be loaded from Drupal\\Core\\Extension\\ModuleHandlerInterface::loadInclude'));
62+
self::assertNotFalse(strpos($error->getMessage(), 'phpstan_fixtures/phpstan_fixtures.fetch.inc could not be loaded from Drupal\\Core\\Extension\\ModuleHandlerInterface::loadInclude'));
6063
}
6164

6265
public function testExtensionTestSuiteAutoloading(): void
@@ -79,9 +82,10 @@ public function testExtensionTestSuiteAutoloading(): void
7982
public function testServiceMapping8()
8083
{
8184
if (version_compare('9.0.0', \Drupal::VERSION) !== 1) {
82-
$this->markTestSkipped('Only tested on Drupal 8.x.x');
85+
self::markTestSkipped('Only tested on Drupal 8.x.x');
8386
}
8487
$errorMessages = [
88+
'The "entity.manager" service is deprecated. You should use the \'entity_type.manager\' service instead.',
8589
'\Drupal calls should be avoided in classes, use dependency injection instead',
8690
'Call to an undefined method Drupal\Core\Entity\EntityManager::thisMethodDoesNotExist().',
8791
'Call to deprecated method getDefinitions() of class Drupal\\Core\\Entity\\EntityManager:
@@ -90,7 +94,7 @@ public function testServiceMapping8()
9094
instead.'
9195
];
9296
$errors = $this->runAnalyze(__DIR__ . '/../fixtures/drupal/modules/phpstan_fixtures/src/TestServicesMappingExtension.php');
93-
$this->assertCount(3, $errors->getErrors());
97+
$this->assertCount(4, $errors->getErrors());
9498
$this->assertCount(0, $errors->getInternalErrors());
9599
foreach ($errors->getErrors() as $key => $error) {
96100
$this->assertEquals($errorMessages[$key], $error->getMessage());
@@ -115,9 +119,20 @@ public function testServiceMapping9()
115119
}
116120

117121
public function testAppRootPseudoService() {
122+
$is_d9 = version_compare('9.0.0', \Drupal::VERSION) !== 1;
118123
$errors = $this->runAnalyze(__DIR__ . '/../fixtures/drupal/modules/phpstan_fixtures/src/AppRootParameter.php');
119-
$this->assertCount(0, $errors->getErrors(), var_export($errors, TRUE));
120-
$this->assertCount(0, $errors->getInternalErrors(), var_export($errors, TRUE));
124+
if ($is_d9) {
125+
$this->assertCount(1, $errors->getErrors(), var_export($errors, TRUE));
126+
self::assertEquals(
127+
'The "app.root" service is deprecated in drupal:9.0.0 and is removed from drupal:10.0.0. Use the app.root parameter instead. See https://www.drupal.org/node/3080612',
128+
$errors->getErrors()[0]->getMessage()
129+
);
130+
$this->assertCount(0, $errors->getInternalErrors(), var_export($errors, TRUE));
131+
}
132+
else {
133+
$this->assertCount(0, $errors->getErrors(), var_export($errors, TRUE));
134+
$this->assertCount(0, $errors->getInternalErrors(), var_export($errors, TRUE));
135+
}
121136
}
122137

123138
public function testThemeSettingsFile() {

0 commit comments

Comments
 (0)