Skip to content

Commit f767648

Browse files
authored
Merge pull request #128 from eiriksm/feat/load-include
Add a rule for loadIncludes. Fix #124
2 parents fc7e1f5 + 8b7c1aa commit f767648

File tree

5 files changed

+133
-2
lines changed

5 files changed

+133
-2
lines changed

extension.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,8 @@ services:
4343
-
4444
class: PHPStan\Reflection\EntityFieldsViaMagicReflectionExtension
4545
tags: [phpstan.broker.propertiesClassReflectionExtension]
46+
-
47+
class: PHPStan\Rules\Drupal\LoadIncludes
48+
tags: [phpstan.rules.rule]
49+
arguments:
50+
- %drupal.drupal_root%

src/Drupal/ExtensionDiscovery.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@ public function __construct($root)
118118
*/
119119
public function scan($type)
120120
{
121+
static $scanresult;
122+
if (!$scanresult) {
123+
$scanresult = [];
124+
}
125+
126+
if (isset($scanresult[$type])) {
127+
return $scanresult[$type];
128+
}
129+
121130
$searchdirs = [];
122131
// Search the core directory.
123132
$searchdirs[static::ORIGIN_CORE] = 'core';
@@ -152,7 +161,8 @@ public function scan($type)
152161
$files = $this->sort($files, $origin_weights);
153162

154163
// Process and return the list of extensions keyed by extension name.
155-
return $this->process($files);
164+
$scanresult[$type] = $this->process($files);
165+
return $scanresult[$type];
156166
}
157167

158168
/**

src/Rules/Drupal/LoadIncludes.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPStan\Rules\Drupal;
4+
5+
use Drupal\Core\Extension\ModuleHandler;
6+
use Drupal\Core\Extension\ModuleHandlerInterface;
7+
use DrupalFinder\DrupalFinder;
8+
use PhpParser\Node;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Drupal\ExtensionDiscovery;
11+
use PHPStan\Reflection\MethodReflection;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\ShouldNotHappenException;
14+
use PHPStan\Type\ObjectType;
15+
16+
class LoadIncludes implements Rule
17+
{
18+
19+
/**
20+
* The project root.
21+
*
22+
* @var string
23+
*/
24+
protected $projectRoot;
25+
26+
/**
27+
* LoadIncludes constructor.
28+
* @param string $project_root
29+
*/
30+
public function __construct(string $project_root)
31+
{
32+
$this->projectRoot = $project_root;
33+
}
34+
35+
public function getNodeType(): string
36+
{
37+
return Node\Expr\MethodCall::class;
38+
}
39+
40+
public function processNode(Node $node, Scope $scope): array
41+
{
42+
assert($node instanceof Node\Expr\MethodCall);
43+
if (!$node->name instanceof Node\Identifier) {
44+
return [];
45+
}
46+
$method_name = $node->name->toString();
47+
if ($method_name !== 'loadInclude') {
48+
return [];
49+
}
50+
$variable = $node->var;
51+
if (!$variable instanceof Node\Expr\Variable) {
52+
return [];
53+
}
54+
$var_name = $variable->name;
55+
if (!is_string($var_name)) {
56+
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()));
57+
}
58+
$type = $scope->getVariableType($var_name);
59+
assert($type instanceof ObjectType);
60+
if (!class_exists($type->getClassName()) && !interface_exists($type->getClassName())) {
61+
throw new ShouldNotHappenException(sprintf('Could not find class for %s from reflection.', get_called_class()));
62+
}
63+
64+
try {
65+
$reflected = new \ReflectionClass($type->getClassName());
66+
if (!$reflected->implementsInterface(ModuleHandlerInterface::class)) {
67+
return [];
68+
}
69+
// Try to invoke it similarily as the module handler itself.
70+
$finder = new DrupalFinder();
71+
$finder->locateRoot($this->projectRoot);
72+
$drupal_root = $finder->getDrupalRoot();
73+
$extensionDiscovery = new ExtensionDiscovery($drupal_root);
74+
$modules = $extensionDiscovery->scan('module');
75+
$module_arg = $node->args[0];
76+
assert($module_arg->value instanceof Node\Scalar\String_);
77+
$type_arg = $node->args[1];
78+
assert($type_arg->value instanceof Node\Scalar\String_);
79+
$name_arg = $node->args[2] ?? null;
80+
81+
if ($name_arg === null) {
82+
$name_arg = $module_arg;
83+
}
84+
assert($name_arg->value instanceof Node\Scalar\String_);
85+
86+
$module_name = $module_arg->value->value;
87+
if (!isset($modules[$module_name])) {
88+
return [];
89+
}
90+
$type_prefix = $name_arg->value->value;
91+
$type_filename = $type_arg->value->value;
92+
$module = $modules[$module_name];
93+
$file = $drupal_root . '/' . $module->getPath() . "/$type_prefix.$type_filename";
94+
if (is_file($file)) {
95+
require_once $file;
96+
return [];
97+
}
98+
return [sprintf('File %s could not be loaded from %s::loadInclude', $file, $type->getClassName())];
99+
} catch (\Throwable $e) {
100+
return [sprintf('A file could not be loaded from %s::loadInclude', $type->getClassName())];
101+
}
102+
}
103+
}

tests/fixtures/drupal/modules/phpstan_fixtures/phpstan_fixtures.module

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,14 @@ function phpstan_fixtures_get_app_root(): string {
1616
$app_root = \Drupal::getContainer()->get('app.root');
1717
return $app_root . '/core/includes/install.inc';
1818
}
19+
20+
function phpstan_fixtures_module_load_includes_test(): array {
21+
$module_handler = \Drupal::moduleHandler();
22+
$module_handler->loadInclude('locale', 'fetch.inc');
23+
return _locale_translation_default_update_options();
24+
}
25+
26+
function phpstan_fixtures_module_load_includes_negative_test(): void {
27+
$module_handler = \Drupal::moduleHandler();
28+
$module_handler->loadInclude('phpstan_fixtures', 'fetch.inc');
29+
}

tests/src/DrupalIntegrationTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,16 @@ public function testDrupalTestInChildSiteContant() {
3838

3939
public function testExtensionReportsError() {
4040
$errors = $this->runAnalyze(__DIR__ . '/../fixtures/drupal/modules/phpstan_fixtures/phpstan_fixtures.module');
41-
$this->assertCount(2, $errors->getErrors(), var_export($errors, true));
41+
$this->assertCount(3, $errors->getErrors(), var_export($errors, true));
4242
$this->assertCount(0, $errors->getInternalErrors(), var_export($errors, true));
4343

4444
$errors = $errors->getErrors();
4545
$error = array_shift($errors);
4646
$this->assertEquals('If condition is always false.', $error->getMessage());
4747
$error = array_shift($errors);
4848
$this->assertEquals('Function phpstan_fixtures_MissingReturnRule() should return string but return statement is missing.', $error->getMessage());
49+
$error = array_shift($errors);
50+
$this->assertStringContainsString('phpstan_fixtures/phpstan_fixtures.fetch.inc could not be loaded from Drupal\\Core\\Extension\\ModuleHandlerInterface::loadInclude', $error->getMessage());
4951
}
5052

5153
public function testExtensionTestSuiteAutoloading() {

0 commit comments

Comments
 (0)