Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ services:
-
class: PHPStan\Reflection\EntityFieldsViaMagicReflectionExtension
tags: [phpstan.broker.propertiesClassReflectionExtension]
-
class: PHPStan\Rules\Drupal\LoadIncludes
tags: [phpstan.rules.rule]
arguments:
- %drupal.drupal_root%
12 changes: 11 additions & 1 deletion src/Drupal/ExtensionDiscovery.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ public function __construct($root)
*/
public function scan($type)
{
static $scanresult;
if (!$scanresult) {
$scanresult = [];
}

if (isset($scanresult[$type])) {
return $scanresult[$type];
}

$searchdirs = [];
// Search the core directory.
$searchdirs[static::ORIGIN_CORE] = 'core';
Expand Down Expand Up @@ -152,7 +161,8 @@ public function scan($type)
$files = $this->sort($files, $origin_weights);

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

/**
Expand Down
103 changes: 103 additions & 0 deletions src/Rules/Drupal/LoadIncludes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php declare(strict_types=1);

namespace PHPStan\Rules\Drupal;

use Drupal\Core\Extension\ModuleHandler;
use Drupal\Core\Extension\ModuleHandlerInterface;
use DrupalFinder\DrupalFinder;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Drupal\ExtensionDiscovery;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Rules\Rule;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\ObjectType;

class LoadIncludes implements Rule
{

/**
* The project root.
*
* @var string
*/
protected $projectRoot;

/**
* LoadIncludes constructor.
* @param string $project_root
*/
public function __construct(string $project_root)
{
$this->projectRoot = $project_root;
}

public function getNodeType(): string
{
return Node\Expr\MethodCall::class;
}

public function processNode(Node $node, Scope $scope): array
{
assert($node instanceof Node\Expr\MethodCall);
if (!$node->name instanceof Node\Identifier) {
return [];
}
$method_name = $node->name->toString();
if ($method_name !== 'loadInclude') {
return [];
}
$variable = $node->var;
if (!$variable instanceof Node\Expr\Variable) {
return [];
}
$var_name = $variable->name;
if (!is_string($var_name)) {
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()));
}
$type = $scope->getVariableType($var_name);
assert($type instanceof ObjectType);
if (!class_exists($type->getClassName()) && !interface_exists($type->getClassName())) {
throw new ShouldNotHappenException(sprintf('Could not find class for %s from reflection.', get_called_class()));
}

try {
$reflected = new \ReflectionClass($type->getClassName());
if (!$reflected->implementsInterface(ModuleHandlerInterface::class)) {
return [];
}
// Try to invoke it similarily as the module handler itself.
$finder = new DrupalFinder();
$finder->locateRoot($this->projectRoot);
$drupal_root = $finder->getDrupalRoot();
$extensionDiscovery = new ExtensionDiscovery($drupal_root);
$modules = $extensionDiscovery->scan('module');
$module_arg = $node->args[0];
assert($module_arg->value instanceof Node\Scalar\String_);
$type_arg = $node->args[1];
assert($type_arg->value instanceof Node\Scalar\String_);
$name_arg = $node->args[2] ?? null;

if ($name_arg === null) {
$name_arg = $module_arg;
}
assert($name_arg->value instanceof Node\Scalar\String_);

$module_name = $module_arg->value->value;
if (!isset($modules[$module_name])) {
return [];
}
$type_prefix = $name_arg->value->value;
$type_filename = $type_arg->value->value;
$module = $modules[$module_name];
$file = $drupal_root . '/' . $module->getPath() . "/$type_prefix.$type_filename";
if (is_file($file)) {
require_once $file;
return [];
}
return [sprintf('File %s could not be loaded from %s::loadInclude', $file, $type->getClassName())];
} catch (\Throwable $e) {
return [sprintf('A file could not be loaded from %s::loadInclude', $type->getClassName())];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,14 @@ function phpstan_fixtures_get_app_root(): string {
$app_root = \Drupal::getContainer()->get('app.root');
return $app_root . '/core/includes/install.inc';
}

function phpstan_fixtures_module_load_includes_test(): array {
$module_handler = \Drupal::moduleHandler();
$module_handler->loadInclude('locale', 'fetch.inc');
return _locale_translation_default_update_options();
}

function phpstan_fixtures_module_load_includes_negative_test(): void {
$module_handler = \Drupal::moduleHandler();
$module_handler->loadInclude('phpstan_fixtures', 'fetch.inc');
}
4 changes: 3 additions & 1 deletion tests/src/DrupalIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,16 @@ public function testDrupalTestInChildSiteContant() {

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

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

public function testExtensionTestSuiteAutoloading() {
Expand Down