Skip to content

Commit 446a990

Browse files
authored
Implement a rule to check deprecated hook implementations (#634)
This will analyse hook implementations in `.inc` and `.module` files. It detects a hook by looking at the pattern `mymodule_[hook]` which is detected from the file-name the function is in. It will replace `mymodule` with `hook` and then try to find a function with that name. For this to work PHPStan Drupal's `DrupalAutoloader` class must be adjusted to load `.api.php` files as well so that the hooks can be discovered. In testing on the Open Social code-base this did not cause any issues. The rule will provide the deprecation message in the error if it's provided, if no deprecation message was provided (i.e. a lonesome `@deprecated` tag) then the hook will simply be shown as deprecated without advice.
1 parent 873c8b6 commit 446a990

File tree

6 files changed

+135
-0
lines changed

6 files changed

+135
-0
lines changed

src/Drupal/DrupalAutoloader.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ public function register(Container $container): void
111111
if (file_exists($module_dir . '/' . $module_name . '.post_update.php')) {
112112
$this->loadAndCatchErrors($module_dir . '/' . $module_name . '.post_update.php');
113113
}
114+
// Add .api.php
115+
if (file_exists($module_dir . '/' . $module_name . '.api.php')) {
116+
$this->loadAndCatchErrors($module_dir . '/' . $module_name . '.api.php');
117+
}
114118
// Add misc .inc that are magically allowed via hook_hook_info.
115119
$magic_hook_info_includes = [
116120
'views',
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace mglaman\PHPStanDrupal\Rules\Deprecations;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Name;
7+
use PhpParser\Node\Stmt\Function_;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\ReflectionProvider;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
13+
class DeprecatedHookImplementation implements Rule
14+
{
15+
16+
/**
17+
* @var \PHPStan\Reflection\ReflectionProvider
18+
*/
19+
protected $reflectionProvider;
20+
21+
public function __construct(ReflectionProvider $reflectionProvider)
22+
{
23+
$this->reflectionProvider = $reflectionProvider;
24+
}
25+
26+
public function getNodeType(): string
27+
{
28+
return Function_::class;
29+
}
30+
31+
public function processNode(Node $node, Scope $scope) : array
32+
{
33+
assert($node instanceof Function_);
34+
if (!str_ends_with($scope->getFile(), ".module") && !str_ends_with($scope->getFile(), ".inc")) {
35+
return [];
36+
}
37+
38+
// We want both name.module and name.views.inc, to resolve to name.
39+
$module_name = explode(".", basename($scope->getFile()))[0];
40+
41+
// Hooks start with their own module's name.
42+
if (!str_starts_with($node->name->toString(), "{$module_name}_")) {
43+
return [];
44+
}
45+
46+
$function_name = $node->name->toString();
47+
$hook_name = substr_replace($function_name, "hook", 0, strlen($module_name));
48+
49+
$hook_name_node = new Name($hook_name);
50+
if (!$this->reflectionProvider->hasFunction($hook_name_node, $scope)) {
51+
return [];
52+
}
53+
54+
$reflection = $this->reflectionProvider->getFunction($hook_name_node, $scope);
55+
if (!$reflection->isDeprecated()->yes()) {
56+
return [];
57+
}
58+
59+
$deprecation_description = $reflection->getDeprecatedDescription();
60+
$deprecation_message = $deprecation_description !== null ? " $deprecation_description" : ".";
61+
62+
return [
63+
RuleErrorBuilder::message(
64+
"Function $function_name implements $hook_name which is deprecated$deprecation_message",
65+
)->build()
66+
];
67+
}
68+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
/**
4+
* A deprecated hook with a message.
5+
*
6+
* @deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. Use hook_other_example instead.
7+
*/
8+
function hook_example() {}
9+
10+
/**
11+
* A deprecated hook without a message.
12+
*
13+
* @deprecated
14+
*/
15+
function hook_example2() {}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name: module_with_deprecated_hooks
2+
type: module
3+
core_version_requirement: ^8 || ^9
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
function module_with_deprecated_hooks_example() {}
4+
5+
function module_with_deprecated_hooks_example2() {}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace mglaman\PHPStanDrupal\Tests\Rules;
4+
5+
use mglaman\PHPStanDrupal\Rules\Deprecations\DeprecatedHookImplementation;
6+
use PHPStan\Reflection\ReflectionProvider;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Testing\RuleTestCase;
9+
10+
/**
11+
* Test the rule to detected deprecated hook implementations.
12+
*/
13+
class DeprecatedHookImplementationTest extends RuleTestCase {
14+
15+
/**
16+
* {@inheritdoc}
17+
*/
18+
protected function getRule(): Rule {
19+
return new DeprecatedHookImplementation(
20+
self::getContainer()->getByType(ReflectionProvider::class)
21+
);
22+
}
23+
24+
/**
25+
* Ensure hook deprecations are flagged with and without reason.
26+
*/
27+
public function testRule() : void {
28+
$this->analyse([__DIR__ . '/../../fixtures/drupal/modules/module_with_deprecated_hooks/module_with_deprecated_hooks.module'], [
29+
[
30+
'Function module_with_deprecated_hooks_example implements hook_example which is deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. Use hook_other_example instead.',
31+
3,
32+
],
33+
[
34+
'Function module_with_deprecated_hooks_example2 implements hook_example2 which is deprecated.',
35+
5,
36+
],
37+
]);
38+
}
39+
40+
}

0 commit comments

Comments
 (0)