Skip to content

Commit 650e728

Browse files
authored
Merge pull request #264 from brambaud/issue/249
Make the discovered Drupal extensions accessible via a service
2 parents bb3dcfe + f2f6826 commit 650e728

File tree

12 files changed

+311
-107
lines changed

12 files changed

+311
-107
lines changed

extension.neon

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ rules:
4646
services:
4747
-
4848
class: mglaman\PHPStanDrupal\Drupal\ServiceMap
49+
-
50+
class: mglaman\PHPStanDrupal\Drupal\ExtensionMap
4951
-
5052
class: mglaman\PHPStanDrupal\Drupal\EntityDataRepository
5153
arguments:
@@ -92,12 +94,12 @@ services:
9294
class: mglaman\PHPStanDrupal\Rules\Drupal\LoadIncludes
9395
tags: [phpstan.rules.rule]
9496
arguments:
95-
- %drupal.drupal_root%
97+
extensionMap: @mglaman\PHPStanDrupal\Drupal\ExtensionMap
9698
-
9799
class: mglaman\PHPStanDrupal\Rules\Drupal\ModuleLoadInclude
98100
tags: [phpstan.rules.rule]
99101
arguments:
100-
- %drupal.drupal_root%
102+
extensionMap: @mglaman\PHPStanDrupal\Drupal\ExtensionMap
101103

102104
-
103105
class: mglaman\PHPStanDrupal\Rules\Deprecations\PluginAnnotationContextDefinitionsRule

src/Drupal/DrupalAutoloader.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ class: Drupal\jsonapi\Routing\JsonApiParamEnhancer
196196
&& class_exists('Drupal\TestTools\PhpUnitCompatibility\PhpUnit8\ClassWriter')) {
197197
\Drupal\TestTools\PhpUnitCompatibility\PhpUnit8\ClassWriter::mutateTestBase($this->autoloader);
198198
}
199+
200+
$extension_map = $container->getByType(ExtensionMap::class);
201+
$extension_map->setExtensions($this->moduleData, $this->themeData, $profiles);
199202
}
200203

201204
protected function loadLegacyIncludes(): void

src/Drupal/Extension.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace mglaman\PHPStanDrupal\Drupal;
44

5+
use Symfony\Component\Yaml\Yaml;
6+
57
/**
68
* Defines an extension (file) object.
79
*
@@ -58,6 +60,16 @@ class Extension
5860
*/
5961
public $origin = '';
6062

63+
/**
64+
* @var array|null
65+
*/
66+
private $info;
67+
68+
/**
69+
* @var string[]|null
70+
*/
71+
private $dependencies;
72+
6173
/**
6274
* Constructs a new Extension object.
6375
*
@@ -109,6 +121,11 @@ public function getPath(): string
109121
return dirname($this->pathname);
110122
}
111123

124+
public function getAbsolutePath(): string
125+
{
126+
return $this->root . DIRECTORY_SEPARATOR . $this->getPath();
127+
}
128+
112129
/**
113130
* Returns the relative path and filename of the extension's info file.
114131
*
@@ -167,4 +184,49 @@ public function load(): bool
167184
}
168185
return false;
169186
}
187+
188+
/**
189+
* @return string[]
190+
*/
191+
public function getDependencies(): array
192+
{
193+
if (\is_array($this->dependencies)) {
194+
return $this->dependencies;
195+
}
196+
197+
$info = $this->parseInfo();
198+
$dependencies = $info['dependencies'] ?? [];
199+
200+
if ($dependencies === []) {
201+
return $this->dependencies = $dependencies;
202+
}
203+
204+
$this->dependencies = [];
205+
206+
// @see \Drupal\Core\Extension\Dependency::createFromString().
207+
foreach ($dependencies as $dependency) {
208+
if (\strpos($dependency, ':') !== false) {
209+
[, $dependency] = \explode(':', $dependency);
210+
}
211+
212+
$parts = \explode('(', $dependency, 2);
213+
$this->dependencies[] = \trim($parts[0]);
214+
}
215+
216+
return $this->dependencies;
217+
}
218+
219+
private function parseInfo(): array
220+
{
221+
if (\is_array($this->info)) {
222+
return $this->info;
223+
}
224+
225+
$infoContent = \file_get_contents(\sprintf('%s/%s', $this->root, $this->getPathname()));
226+
if (false === $infoContent) {
227+
throw new \RuntimeException(\sprintf('Cannot read "%s', $this->getPathname()));
228+
}
229+
230+
return $this->info = Yaml::parse($infoContent);
231+
}
170232
}

src/Drupal/ExtensionMap.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace mglaman\PHPStanDrupal\Drupal;
4+
5+
final class ExtensionMap
6+
{
7+
/** @var array<string, Extension> */
8+
private static $modules = [];
9+
10+
/** @var array<string, Extension> */
11+
private static $themes = [];
12+
13+
/** @var array<string, Extension> */
14+
private static $profiles = [];
15+
16+
/**
17+
* @return Extension[]
18+
*/
19+
public function getModules(): array
20+
{
21+
return self::$modules;
22+
}
23+
24+
public function getModule(string $name): ?Extension
25+
{
26+
return self::$modules[$name] ?? null;
27+
}
28+
29+
/**
30+
* @return Extension[]
31+
*/
32+
public function getThemes(): array
33+
{
34+
return self::$themes;
35+
}
36+
37+
public function getTheme(string $name): ?Extension
38+
{
39+
return self::$themes[$name] ?? null;
40+
}
41+
42+
/**
43+
* @return Extension[]
44+
*/
45+
public function getProfiles(): array
46+
{
47+
return self::$profiles;
48+
}
49+
50+
public function getProfile(string $name): ?Extension
51+
{
52+
return self::$profiles[$name] ?? null;
53+
}
54+
55+
/**
56+
* @param array<int, Extension> $modules
57+
* @param array<int, Extension> $themes
58+
* @param array<int, Extension> $profiles
59+
*/
60+
public function setExtensions(array $modules, array $themes, array $profiles): void
61+
{
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;
82+
}
83+
}

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())

0 commit comments

Comments
 (0)