Skip to content

Commit 9d6f6eb

Browse files
committed
Inject ReflectionProvider and use Type system for plugin manager rule
1 parent 84a2113 commit 9d6f6eb

File tree

4 files changed

+171
-14
lines changed

4 files changed

+171
-14
lines changed

extension.neon

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ parametersSchema:
3939
))
4040
])
4141
rules:
42-
- mglaman\PHPStanDrupal\Rules\Classes\PluginManagerInspectionRule
4342
- mglaman\PHPStanDrupal\Rules\Drupal\Coder\DiscouragedFunctionsRule
4443
- mglaman\PHPStanDrupal\Rules\Drupal\GlobalDrupalDependencyInjectionRule
4544
- mglaman\PHPStanDrupal\Rules\Drupal\PluginManager\PluginManagerSetsCacheBackendRule
@@ -84,6 +83,11 @@ services:
8483
tags: [phpstan.rules.rule]
8584
arguments:
8685
reflectionProvider: @reflectionProvider
86+
-
87+
class: mglaman\PHPStanDrupal\Rules\Classes\PluginManagerInspectionRule
88+
tags: [phpstan.rules.rule]
89+
arguments:
90+
reflectionProvider: @reflectionProvider
8791
-
8892
class: mglaman\PHPStanDrupal\Rules\Drupal\LoadIncludes
8993
tags: [phpstan.rules.rule]

src/Rules/Classes/PluginManagerInspectionRule.php

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,41 @@
44

55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\ReflectionProvider;
78
use PHPStan\Rules\Rule;
89
use PHPStan\ShouldNotHappenException;
10+
use PHPStan\Type\ObjectType;
911

12+
/**
13+
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Class_>
14+
*/
1015
class PluginManagerInspectionRule implements Rule
1116
{
17+
/** @var ReflectionProvider */
18+
private $reflectionProvider;
19+
public function __construct(ReflectionProvider $reflectionProvider)
20+
{
21+
$this->reflectionProvider = $reflectionProvider;
22+
}
23+
1224
public function getNodeType(): string
1325
{
1426
return Node\Stmt\Class_::class;
1527
}
1628

1729
public function processNode(Node $node, Scope $scope): array
1830
{
19-
assert($node instanceof Node\Stmt\Class_);
20-
31+
if ($node->namespacedName === null) {
32+
// anonymous class
33+
return [];
34+
}
2135
if ($node->extends === null) {
2236
return [];
2337
}
24-
25-
// If the class does not extend the default plugin manager, skip it.
26-
// @todo inspect interfaces and see if it implements PluginManagerInterface.
27-
if ($node->extends->toString() !== 'Drupal\Core\Plugin\DefaultPluginManager') {
38+
$className = (string) $node->namespacedName;
39+
$pluginManagerType = new ObjectType($className);
40+
$pluginManagerInterfaceType = new ObjectType('\Drupal\Component\Plugin\PluginManagerInterface');
41+
if (!$pluginManagerInterfaceType->isSuperTypeOf($pluginManagerType)->yes()) {
2842
return [];
2943
}
3044

@@ -98,15 +112,11 @@ private function inspectYamlPluginManager(Node\Stmt\Class_ $class): array
98112
{
99113
$errors = [];
100114

101-
$fqn = $class->namespacedName;
102-
$reflection = new \ReflectionClass($fqn->toString());
115+
$fqn = (string) $class->namespacedName;
116+
$reflection = $this->reflectionProvider->getClass($fqn);
103117
$constructor = $reflection->getConstructor();
104118

105-
if ($constructor === null) {
106-
throw new ShouldNotHappenException();
107-
}
108-
109-
if ($constructor->class !== $fqn->toString()) {
119+
if ($constructor->getDeclaringClass()->getName() !== $fqn) {
110120
$errors[] = sprintf('%s must override __construct if using YAML plugins.', $fqn);
111121
} else {
112122
foreach ($class->stmts as $stmt) {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Drupal\phpstan_fixtures;
6+
7+
use Drupal\Component\Plugin\Exception\PluginException;
8+
use Drupal\Core\Cache\CacheBackendInterface;
9+
use Drupal\Core\Extension\ModuleHandlerInterface;
10+
use Drupal\Core\Extension\ThemeHandlerInterface;
11+
use Drupal\Core\Plugin\DefaultPluginManager;
12+
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
13+
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
14+
15+
/**
16+
* Provides the default example manager.
17+
*/
18+
class ExamplePluginManager extends DefaultPluginManager {
19+
20+
/**
21+
* The theme handler.
22+
*
23+
* @var \Drupal\Core\Extension\ThemeHandlerInterface
24+
*/
25+
protected $themeHandler;
26+
27+
/**
28+
* Provides default values for all style_plugin plugins.
29+
*
30+
* @var array
31+
*/
32+
protected $defaults = [
33+
// Add required and optional plugin properties.
34+
'id' => '',
35+
'enabled' => TRUE,
36+
'label' => '',
37+
'description' => '',
38+
'render' => [],
39+
];
40+
41+
/**
42+
* Constructor.
43+
*
44+
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
45+
* The module handler.
46+
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
47+
* The theme handler.
48+
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
49+
* Cache backend instance to use.
50+
*/
51+
public function __construct(
52+
ModuleHandlerInterface $module_handler,
53+
ThemeHandlerInterface $theme_handler,
54+
CacheBackendInterface $cache_backend
55+
) {
56+
$this->moduleHandler = $module_handler;
57+
$this->themeHandler = $theme_handler;
58+
$this->alterInfo('ui_examples_examples');
59+
$this->setCacheBackend($cache_backend, 'ui_examples', ['ui_examples']);
60+
}
61+
62+
/**
63+
* {@inheritdoc}
64+
*/
65+
protected function getDiscovery() {
66+
$this->discovery = new YamlDiscovery('ui_examples', $this->moduleHandler->getModuleDirectories() + $this->themeHandler->getThemeDirectories());
67+
$this->discovery->addTranslatableProperty('label', 'label_context');
68+
$this->discovery->addTranslatableProperty('description', 'description_context');
69+
$this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
70+
return $this->discovery;
71+
}
72+
73+
/**
74+
* {@inheritdoc}
75+
*/
76+
public function processDefinition(&$definition, $plugin_id) : void {
77+
parent::processDefinition($definition, $plugin_id);
78+
// @todo Add validation of the plugin definition here.
79+
if (empty($definition['id'])) {
80+
throw new PluginException(sprintf('Example plugin property (%s) definition "id" is required.', $plugin_id));
81+
}
82+
}
83+
84+
/**
85+
* {@inheritdoc}
86+
*/
87+
protected function alterDefinitions(&$definitions) {
88+
foreach ($definitions as $definition_key => $definition_info) {
89+
if (isset($definition_info['enabled']) && !$definition_info['enabled']) {
90+
unset($definitions[$definition_key]);
91+
continue;
92+
}
93+
}
94+
95+
parent::alterDefinitions($definitions);
96+
}
97+
98+
/**
99+
* {@inheritdoc}
100+
*/
101+
protected function providerExists($provider) : bool {
102+
return $this->moduleHandler->moduleExists($provider) || $this->themeHandler->themeExists($provider);
103+
}
104+
105+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace mglaman\PHPStanDrupal\Tests\Rules;
4+
5+
use mglaman\PHPStanDrupal\Rules\Classes\PluginManagerInspectionRule;
6+
use mglaman\PHPStanDrupal\Tests\DrupalRuleTestCase;
7+
8+
final class PluginManagerInspectionRuleTest extends DrupalRuleTestCase
9+
{
10+
11+
protected function getRule(): \PHPStan\Rules\Rule
12+
{
13+
return new PluginManagerInspectionRule(
14+
$this->createReflectionProvider()
15+
);
16+
}
17+
18+
/**
19+
* @dataProvider pluginManagerData
20+
*/
21+
public function testRule(string $path, array $errorMessages): void
22+
{
23+
$this->analyse([$path], $errorMessages);
24+
}
25+
26+
public function pluginManagerData(): \Generator
27+
{
28+
yield 'BreakpointManager' => [
29+
__DIR__ . '/../../fixtures/drupal/core/modules/breakpoint/src/BreakpointManager.php',
30+
[]
31+
];
32+
yield 'ExamplePluginManager' => [
33+
__DIR__ . '/../../fixtures/drupal/modules/phpstan_fixtures/src/ExamplePluginManager.php',
34+
[]
35+
];
36+
}
37+
38+
}

0 commit comments

Comments
 (0)