Skip to content

Commit f2f6826

Browse files
committed
Leverage ExtensionMap with load include rules
1 parent 5e30d61 commit f2f6826

File tree

8 files changed

+145
-116
lines changed

8 files changed

+145
-116
lines changed

extension.neon

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,12 @@ services:
9494
class: mglaman\PHPStanDrupal\Rules\Drupal\LoadIncludes
9595
tags: [phpstan.rules.rule]
9696
arguments:
97-
- %drupal.drupal_root%
97+
extensionMap: @mglaman\PHPStanDrupal\Drupal\ExtensionMap
9898
-
9999
class: mglaman\PHPStanDrupal\Rules\Drupal\ModuleLoadInclude
100100
tags: [phpstan.rules.rule]
101101
arguments:
102-
- %drupal.drupal_root%
102+
extensionMap: @mglaman\PHPStanDrupal\Drupal\ExtensionMap
103103

104104
-
105105
class: mglaman\PHPStanDrupal\Rules\Deprecations\PluginAnnotationContextDefinitionsRule

src/Drupal/Extension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ public function getPath(): string
121121
return dirname($this->pathname);
122122
}
123123

124+
public function getAbsolutePath(): string
125+
{
126+
return $this->root . DIRECTORY_SEPARATOR . $this->getPath();
127+
}
128+
124129
/**
125130
* Returns the relative path and filename of the extension's info file.
126131
*

src/Drupal/ExtensionMap.php

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
final class ExtensionMap
66
{
7-
/** @var Extension[] */
7+
/** @var array<string, Extension> */
88
private static $modules = [];
99

10-
/** @var Extension[] */
10+
/** @var array<string, Extension> */
1111
private static $themes = [];
1212

13-
/** @var Extension[] */
13+
/** @var array<string, Extension> */
1414
private static $profiles = [];
1515

1616
/**
@@ -53,14 +53,31 @@ public function getProfile(string $name): ?Extension
5353
}
5454

5555
/**
56-
* @param Extension[] $modules
57-
* @param Extension[] $themes
58-
* @param Extension[] $profiles
56+
* @param array<int, Extension> $modules
57+
* @param array<int, Extension> $themes
58+
* @param array<int, Extension> $profiles
5959
*/
6060
public function setExtensions(array $modules, array $themes, array $profiles): void
6161
{
62-
self::$modules = $modules;
63-
self::$themes = $themes;
64-
self::$profiles = $profiles;
62+
self::$modules = self::keyByExtensionName($modules);
63+
self::$themes = self::keyByExtensionName($themes);
64+
self::$profiles = self::keyByExtensionName($profiles);
65+
}
66+
67+
/**
68+
* @param array<int, Extension> $extensions
69+
* @return array<string, Extension>
70+
*/
71+
private static function keyByExtensionName(array $extensions): array
72+
{
73+
// PHP 7.4 returns array|false, PHP 8.0 only returns an array.
74+
// Make PHPStan happy. When PHP 7.4 is dropped, reduce to a single
75+
// return.
76+
$combined = array_combine(array_map(static function (Extension $extension) {
77+
return $extension->getName();
78+
}, $extensions), $extensions);
79+
// @phpstan-ignore-next-line
80+
assert(is_array($combined));
81+
return $combined;
6582
}
6683
}

src/Rules/Drupal/LoadIncludeBase.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace mglaman\PHPStanDrupal\Rules\Drupal;
4+
5+
use mglaman\PHPStanDrupal\Drupal\ExtensionMap;
6+
use PhpParser\Node;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Type\Constant\ConstantStringType;
10+
11+
abstract class LoadIncludeBase implements Rule
12+
{
13+
14+
/**
15+
* @var \mglaman\PHPStanDrupal\Drupal\ExtensionMap
16+
*/
17+
protected $extensionMap;
18+
19+
public function __construct(ExtensionMap $extensionMap)
20+
{
21+
$this->extensionMap = $extensionMap;
22+
}
23+
24+
private function getStringArgValue(Node\Expr $expr, Scope $scope): ?string
25+
{
26+
$type = $scope->getType($expr);
27+
if ($type instanceof ConstantStringType) {
28+
return $type->getValue();
29+
}
30+
return null;
31+
}
32+
33+
protected function parseLoadIncludeArgs(Node\Arg $module, Node\Arg $type, ?Node\Arg $name, Scope $scope): array
34+
{
35+
$moduleName = $this->getStringArgValue($module->value, $scope);
36+
if ($moduleName === null) {
37+
return [];
38+
}
39+
$fileType = $this->getStringArgValue($type->value, $scope);
40+
if ($fileType === null) {
41+
return [];
42+
}
43+
$baseName = null;
44+
if ($name !== null) {
45+
$baseName = $this->getStringArgValue($name->value, $scope);
46+
}
47+
if ($baseName === null) {
48+
$baseName = $moduleName;
49+
}
50+
51+
return [$moduleName, "$baseName.$fileType"];
52+
}
53+
}

src/Rules/Drupal/LoadIncludes.php

Lines changed: 22 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,15 @@
33
namespace mglaman\PHPStanDrupal\Rules\Drupal;
44

55
use Drupal\Core\Extension\ModuleHandlerInterface;
6-
use DrupalFinder\DrupalFinder;
7-
use mglaman\PHPStanDrupal\Drupal\ExtensionDiscovery;
86
use PhpParser\Node;
97
use PHPStan\Analyser\Scope;
10-
use PHPStan\Rules\Rule;
118
use PHPStan\Rules\RuleErrorBuilder;
129
use PHPStan\ShouldNotHappenException;
1310
use PHPStan\Type\ObjectType;
1411

15-
class LoadIncludes implements Rule
12+
class LoadIncludes extends LoadIncludeBase
1613
{
1714

18-
/**
19-
* The project root.
20-
*
21-
* @var string
22-
*/
23-
protected $projectRoot;
24-
25-
/**
26-
* LoadIncludes constructor.
27-
* @param string $project_root
28-
*/
29-
public function __construct(string $project_root)
30-
{
31-
$this->projectRoot = $project_root;
32-
}
33-
3415
public function getNodeType(): string
3516
{
3617
return Node\Expr\MethodCall::class;
@@ -46,13 +27,17 @@ public function processNode(Node $node, Scope $scope): array
4627
if ($method_name !== 'loadInclude') {
4728
return [];
4829
}
30+
$args = $node->getArgs();
31+
if (\count($args) < 2) {
32+
return [];
33+
}
4934
$variable = $node->var;
5035
if (!$variable instanceof Node\Expr\Variable) {
5136
return [];
5237
}
5338
$var_name = $variable->name;
5439
if (!is_string($var_name)) {
55-
throw new ShouldNotHappenException(sprintf('Expected string for variable in %s, please open an issue on GitHub https://github.com/mglaman/phpstan-drupal/issues', get_called_class()));
40+
throw new ShouldNotHappenException(sprintf('Expected string for variable in %s, please open an issue on GitHub https://github.com/mglaman/phpstan-drupal/issues', static::class));
5641
}
5742
$moduleHandlerInterfaceType = new ObjectType(ModuleHandlerInterface::class);
5843
$variableType = $scope->getVariableType($var_name);
@@ -61,43 +46,31 @@ public function processNode(Node $node, Scope $scope): array
6146
}
6247

6348
try {
64-
// Try to invoke it similarily as the module handler itself.
65-
$finder = new DrupalFinder();
66-
$finder->locateRoot($this->projectRoot);
67-
$drupal_root = $finder->getDrupalRoot();
68-
$extensionDiscovery = new ExtensionDiscovery($drupal_root);
69-
$modules = $extensionDiscovery->scan('module');
70-
$module_arg = $node->args[0];
71-
assert($module_arg instanceof Node\Arg);
72-
assert($module_arg->value instanceof Node\Scalar\String_);
73-
$type_arg = $node->args[1];
74-
assert($type_arg instanceof Node\Arg);
75-
assert($type_arg->value instanceof Node\Scalar\String_);
76-
$name_arg = $node->args[2] ?? null;
77-
78-
if ($name_arg === null) {
79-
$name_arg = $module_arg;
49+
// Try to invoke it similarly as the module handler itself.
50+
[$moduleName, $filename] = $this->parseLoadIncludeArgs($args[0], $args[1], $args[2] ?? null, $scope);
51+
$module = $this->extensionMap->getModule($moduleName);
52+
if ($module === null) {
53+
return [
54+
RuleErrorBuilder::message(sprintf(
55+
'File %s could not be loaded from %s::loadInclude because %s module is not found.',
56+
$filename,
57+
ModuleHandlerInterface::class,
58+
$moduleName
59+
))
60+
->line($node->getLine())
61+
->build()
62+
];
8063
}
81-
assert($name_arg instanceof Node\Arg);
82-
assert($name_arg->value instanceof Node\Scalar\String_);
8364

84-
$module_name = $module_arg->value->value;
85-
if (!isset($modules[$module_name])) {
86-
// @todo return error that module is missing.
87-
return [];
88-
}
89-
$type_prefix = $name_arg->value->value;
90-
$type_filename = $type_arg->value->value;
91-
$module = $modules[$module_name];
92-
$file = $drupal_root . '/' . $module->getPath() . "/$type_prefix.$type_filename";
65+
$file = $module->getAbsolutePath() . DIRECTORY_SEPARATOR . $filename;
9366
if (is_file($file)) {
9467
require_once $file;
9568
return [];
9669
}
9770
return [
9871
RuleErrorBuilder::message(sprintf(
9972
'File %s could not be loaded from %s::loadInclude',
100-
$file,
73+
$module->getPath() . DIRECTORY_SEPARATOR . $filename,
10174
ModuleHandlerInterface::class
10275
))
10376
->line($node->getLine())

src/Rules/Drupal/ModuleLoadInclude.php

Lines changed: 33 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,19 @@
22

33
namespace mglaman\PHPStanDrupal\Rules\Drupal;
44

5-
use DrupalFinder\DrupalFinder;
65
use PhpParser\Node;
76
use PHPStan\Analyser\Scope;
8-
use mglaman\PHPStanDrupal\Drupal\ExtensionDiscovery;
9-
use PHPStan\Rules\Rule;
7+
use PHPStan\Rules\RuleErrorBuilder;
108

119
/**
1210
* Handles module_load_include dynamic file loading.
1311
*
1412
* @note may become deprecated and removed in D10
1513
* @see https://www.drupal.org/project/drupal/issues/697946
1614
*/
17-
class ModuleLoadInclude implements Rule
15+
class ModuleLoadInclude extends LoadIncludeBase
1816
{
1917

20-
/**
21-
* The project root.
22-
*
23-
* @var string
24-
*/
25-
protected $projectRoot;
26-
27-
/**
28-
* ModuleLoadInclude constructor.
29-
* @param string $project_root
30-
*/
31-
public function __construct(string $project_root)
32-
{
33-
$this->projectRoot = $project_root;
34-
}
35-
3618
public function getNodeType(): string
3719
{
3820
return Node\Expr\FuncCall::class;
@@ -48,44 +30,45 @@ public function processNode(Node $node, Scope $scope): array
4830
if ($name !== 'module_load_include') {
4931
return [];
5032
}
33+
$args = $node->getArgs();
34+
if (\count($args) < 2) {
35+
return [];
36+
}
5137

5238
try {
53-
// Try to invoke it similarily as the module handler itself.
54-
$finder = new DrupalFinder();
55-
$finder->locateRoot($this->projectRoot);
56-
$drupal_root = $finder->getDrupalRoot();
57-
$extensionDiscovery = new ExtensionDiscovery($drupal_root);
58-
$modules = $extensionDiscovery->scan('module');
59-
$type_arg = $node->args[0];
60-
assert($type_arg instanceof Node\Arg);
61-
assert($type_arg->value instanceof Node\Scalar\String_);
62-
$module_arg = $node->args[1];
63-
assert($module_arg instanceof Node\Arg);
64-
assert($module_arg->value instanceof Node\Scalar\String_);
65-
$name_arg = $node->args[2] ?? null;
66-
67-
if ($name_arg === null) {
68-
$name_arg = $module_arg;
69-
}
70-
assert($name_arg instanceof Node\Arg);
71-
assert($name_arg->value instanceof Node\Scalar\String_);
72-
73-
$module_name = $module_arg->value->value;
74-
if (!isset($modules[$module_name])) {
75-
// @todo return error that the module does not exist.
76-
return [];
39+
// Try to invoke it similarly as the module handler itself.
40+
[$moduleName, $filename] = $this->parseLoadIncludeArgs($args[1], $args[0], $args[2] ?? null, $scope);
41+
$module = $this->extensionMap->getModule($moduleName);
42+
if ($module === null) {
43+
return [
44+
RuleErrorBuilder::message(sprintf(
45+
'File %s could not be loaded from module_load_include because %s module is not found.',
46+
$filename,
47+
$moduleName
48+
))
49+
->line($node->getLine())
50+
->build()
51+
];
7752
}
78-
$type_prefix = $name_arg->value->value;
79-
$type_filename = $type_arg->value->value;
80-
$module = $modules[$module_name];
81-
$file = $drupal_root . '/' . $module->getPath() . "/$type_prefix.$type_filename";
53+
$file = $module->getAbsolutePath() . DIRECTORY_SEPARATOR . $filename;
8254
if (is_file($file)) {
8355
require_once $file;
8456
return [];
8557
}
86-
return [sprintf('File %s could not be loaded from module_load_include', $file)];
58+
return [
59+
RuleErrorBuilder::message(sprintf(
60+
'File %s could not be loaded from module_load_include.',
61+
$module->getPath() . DIRECTORY_SEPARATOR . $filename
62+
))
63+
->line($node->getLine())
64+
->build()
65+
];
8766
} catch (\Throwable $e) {
88-
return ['A file could not be loaded from module_load_include'];
67+
return [
68+
RuleErrorBuilder::message('A file could not be loaded from module_load_include')
69+
->line($node->getLine())
70+
->build()
71+
];
8972
}
9073
}
9174
}

tests/src/Rules/LoadIncludesRuleTest.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ final class LoadIncludesRuleTest extends DrupalRuleTestCase
99
{
1010
protected function getRule(): \PHPStan\Rules\Rule
1111
{
12-
$params = self::getContainer()->getParameter('drupal');
13-
return new LoadIncludes($params['drupal_root']);
12+
return self::getContainer()->getByType(LoadIncludes::class);
1413
}
1514

1615
public function testRule(): void
@@ -20,7 +19,7 @@ public function testRule(): void
2019
],
2120
[
2221
[
23-
'File tests/fixtures/drupal/modules/phpstan_fixtures/phpstan_fixtures.fetch.inc could not be loaded from Drupal\Core\Extension\ModuleHandlerInterface::loadInclude',
22+
'File modules/phpstan_fixtures/phpstan_fixtures.fetch.inc could not be loaded from Drupal\Core\Extension\ModuleHandlerInterface::loadInclude',
2423
30
2524
]
2625
]);

0 commit comments

Comments
 (0)