Skip to content
Open
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@ parameters:
# Flags plugin managers that do not set a cache backend.
pluginManagerSetsCacheBackendRule: false

# Flags plugin managers that pass a non-attribute (annotation) class as
# the plugin definition attribute name to DefaultPluginManager::__construct().
# Annotation-only discovery is deprecated in drupal:11.2.0 and removed in
# drupal:12.0.0. Also reports static property and constructor parameter
# defaults. Does nothing when AttributeInterface is unavailable (Drupal < 10.2).
pluginManagerAttributeDiscoveryRule: false

# Validates module_load_include() calls and loads the include for
# the rest of the analysis.
moduleLoadInclude: false
Expand Down
2 changes: 2 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ parameters:
accessResultConditionRule: true
classExtendsInternalClassRule: true
pluginManagerInspectionRule: false
pluginManagerAttributeDiscoveryRule: false
cacheableDependencyRule: true
hookFormAlterRule: true
loggerFromFactoryPropertyAssignmentRule: true
Expand Down Expand Up @@ -311,6 +312,7 @@ parametersSchema:
accessResultConditionRule: boolean()
classExtendsInternalClassRule: boolean()
pluginManagerInspectionRule: boolean()
pluginManagerAttributeDiscoveryRule: boolean()
cacheableDependencyRule: boolean()
hookFormAlterRule: boolean()
loggerFromFactoryPropertyAssignmentRule: boolean()
Expand Down
8 changes: 8 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ conditionalTags:
phpstan.rules.rule: %drupal.rules.classExtendsInternalClassRule%
mglaman\PHPStanDrupal\Rules\Classes\PluginManagerInspectionRule:
phpstan.rules.rule: %drupal.rules.pluginManagerInspectionRule%
mglaman\PHPStanDrupal\Rules\Drupal\PluginManager\PluginManagerAttributeDiscoveryRule:
phpstan.rules.rule: %drupal.rules.pluginManagerAttributeDiscoveryRule%
mglaman\PHPStanDrupal\Rules\Drupal\PluginManager\PluginManagerAttributeDiscoveryDefaultRule:
phpstan.rules.rule: %drupal.rules.pluginManagerAttributeDiscoveryRule%
mglaman\PHPStanDrupal\Rules\Drupal\CacheableDependencyRule:
phpstan.rules.rule: %drupal.rules.cacheableDependencyRule%
mglaman\PHPStanDrupal\Rules\Drupal\LoggerFromFactoryPropertyAssignmentRule:
Expand Down Expand Up @@ -77,6 +81,10 @@ services:
class: mglaman\PHPStanDrupal\Rules\Classes\ClassExtendsInternalClassRule
-
class: mglaman\PHPStanDrupal\Rules\Classes\PluginManagerInspectionRule
-
class: mglaman\PHPStanDrupal\Rules\Drupal\PluginManager\PluginManagerAttributeDiscoveryRule
-
class: mglaman\PHPStanDrupal\Rules\Drupal\PluginManager\PluginManagerAttributeDiscoveryDefaultRule
-
class: mglaman\PHPStanDrupal\Rules\Drupal\CacheableDependencyRule
-
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php declare(strict_types=1);

namespace mglaman\PHPStanDrupal\Rules\Drupal\PluginManager;

use PhpParser\Node;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;

/**
* Shared helpers for the plugin manager attribute discovery rules.
*
* @template TNodeType of \PhpParser\Node
* @implements Rule<TNodeType>
*/
abstract class AbstractPluginManagerAttributeDiscoveryRule implements Rule
{
protected const DEFAULT_PLUGIN_MANAGER = 'Drupal\Core\Plugin\DefaultPluginManager';

protected const ATTRIBUTE_INTERFACE = 'Drupal\Component\Plugin\Attribute\AttributeInterface';

private const ATTRIBUTE_ARGUMENT_NAME = 'plugin_definition_attribute_name';

private const ATTRIBUTE_ARGUMENT_POSITION = 4;

protected ReflectionProvider $reflectionProvider;

public function __construct(ReflectionProvider $reflectionProvider)
{
$this->reflectionProvider = $reflectionProvider;
}

/**
* Whether the class directly extends DefaultPluginManager's constructor.
*
* Only the constructor declared by DefaultPluginManager itself has the
* attribute argument. An intermediate parent may have its own signature.
*/
protected function callsDefaultPluginManagerConstructor(ClassReflection $classReflection): bool
{
if ($classReflection->isAnonymous() || $classReflection->isTrait() || !$classReflection->is(self::DEFAULT_PLUGIN_MANAGER)) {
return false;
}
$parentClass = $classReflection->getParentClass();
if ($parentClass === null || !$parentClass->hasConstructor()) {
return false;
}
return $parentClass->getConstructor()->getDeclaringClass()->getName() === self::DEFAULT_PLUGIN_MANAGER;
}

protected function isParentConstructorCall(StaticCall $node): bool
{
return $node->class instanceof Node\Name
&& $node->class->toLowerString() === 'parent'
&& $node->name instanceof Node\Identifier
&& $node->name->toLowerString() === '__construct';
}

/**
* Returns the class when it resolves and is not an attribute class.
*/
protected function resolveNonAttributeClass(string $className): ?ClassReflection
{
// Drupal < 10.2 has no attribute interface. Without it nothing can be
// told apart from an attribute class, so never report.
if (!$this->reflectionProvider->hasClass(self::ATTRIBUTE_INTERFACE)) {
return null;
}
$className = ltrim($className, '\\');
// Only report when certain: the class must be resolvable.
if ($className === '' || !$this->reflectionProvider->hasClass($className)) {
return null;
}
$class = $this->reflectionProvider->getClass($className);
return $class->is(self::ATTRIBUTE_INTERFACE) ? null : $class;
}

protected function findAttributeArgument(StaticCall $node): ?Node\Arg
{
$position = 0;
foreach ($node->getArgs() as $arg) {
if ($arg->unpack) {
return null;
}
if ($arg->name !== null) {
if ($arg->name->toString() === self::ATTRIBUTE_ARGUMENT_NAME) {
return $arg;
}
continue;
}
if ($position === self::ATTRIBUTE_ARGUMENT_POSITION) {
return $arg;
}
$position++;
}
return null;
}
}
Loading
Loading