diff --git a/phpcs.xml b/phpcs.xml index 98d3782b..71f7f9b9 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,6 +1,6 @@ - + diff --git a/src/Drupal/DrupalAutoloader.php b/src/Drupal/DrupalAutoloader.php index 0bf28fd6..7abcb292 100644 --- a/src/Drupal/DrupalAutoloader.php +++ b/src/Drupal/DrupalAutoloader.php @@ -32,52 +32,51 @@ use function ucwords; use function usort; +/** + * Bootstraps a Drupal site for analysis from PHPStan's bootstrap file. + * + * @internal + */ class DrupalAutoloader { - /** - * @var \Composer\Autoload\ClassLoader - */ - private $autoloader; + private ClassLoader $autoloader; - /** - * @var string - */ - private $drupalRoot; + private string $drupalRoot; /** * List of available modules. * * @var Extension[] */ - protected $moduleData = []; + protected array $moduleData = []; /** * List of available themes. * * @var Extension[] */ - protected $themeData = []; + protected array $themeData = []; /** * @var array> */ - private $serviceMap = []; + private array $serviceMap = []; /** * @var array */ - private $serviceYamls = []; + private array $serviceYamls = []; /** * @var array */ - private $serviceClassProviders = []; + private array $serviceClassProviders = []; /** - * @var array + * @var array */ - private $namespaces = []; + private array $namespaces = []; public function register(Container $container): void { diff --git a/src/Drupal/DrupalServiceDefinition.php b/src/Drupal/DrupalServiceDefinition.php index 2c528db3..c1b894e7 100644 --- a/src/Drupal/DrupalServiceDefinition.php +++ b/src/Drupal/DrupalServiceDefinition.php @@ -11,52 +11,23 @@ class DrupalServiceDefinition { - /** - * @var string - */ - private $id; - - /** - * @var string|null - */ - private $class; - - /** - * @var bool - */ - private $public; - - /** - * @var bool - */ - private $deprecated = false; - - /** - * @var string|null - */ - private $deprecationTemplate; + private const DEFAULT_DEPRECATION_TEMPLATE = 'The "%service_id%" service is deprecated. You should stop using it, as it will soon be removed.'; - /** - * @var string - */ - private static $defaultDeprecationTemplate = 'The "%service_id%" service is deprecated. You should stop using it, as it will soon be removed.'; + private bool $deprecated = false; - /** - * @var string|null - */ - private $alias; + private ?string $deprecationTemplate = null; /** * @var array */ - private $decorators = []; - - public function __construct(string $id, ?string $class, bool $public = true, ?string $alias = null) - { - $this->id = $id; - $this->class = $class; - $this->public = $public; - $this->alias = $alias; + private array $decorators = []; + + public function __construct( + private readonly string $id, + private readonly ?string $class, + private readonly bool $public = true, + private readonly ?string $alias = null + ) { } public function setDeprecated(bool $status = true, ?string $template = null): void @@ -65,33 +36,21 @@ public function setDeprecated(bool $status = true, ?string $template = null): vo $this->deprecationTemplate = $template; } - /** - * @return string - */ public function getId(): string { return $this->id; } - /** - * @return string|null - */ public function getClass(): ?string { return $this->class; } - /** - * @return bool - */ public function isPublic(): bool { return $this->public; } - /** - * @return string|null - */ public function getAlias(): ?string { return $this->alias; @@ -104,7 +63,7 @@ public function isDeprecated(): bool public function getDeprecatedDescription(): string { - return str_replace('%service_id%', $this->id, $this->deprecationTemplate ?? self::$defaultDeprecationTemplate); + return str_replace('%service_id%', $this->id, $this->deprecationTemplate ?? self::DEFAULT_DEPRECATION_TEMPLATE); } public function getType(): Type diff --git a/src/Drupal/Extension.php b/src/Drupal/Extension.php index 7a135f28..9d9a274b 100644 --- a/src/Drupal/Extension.php +++ b/src/Drupal/Extension.php @@ -8,74 +8,38 @@ use function file_get_contents; use function is_array; use function sprintf; -use function strpos; +use function str_contains; use function trim; /** * Defines an extension (file) object. * * Bundled version of \Drupal\Core\Extension\Extension. + * + * @internal */ class Extension { /** - * The type of the extension (e.g., 'module'). - * - * @var string + * The subpath of the extension below the search path it was found in. */ - protected $type; + public string $subpath = ''; /** - * The relative pathname of the extension (e.g., - * 'core/modules/node/node.info.yml'). - * - * @var string - */ - protected $pathname; - - /** - * The filename of the main extension file (e.g., 'node.module'). - * - * @var string|null + * The originating search path directory (e.g., 'core'). */ - protected $filename; + public string $origin = ''; /** - * An SplFileInfo instance for the extension's info file. - * - * Note that SplFileInfo is a PHP resource and resources cannot be serialized. - * - * @var ?\SplFileInfo - */ - protected $splFileInfo; - - /** - * The app root. - * - * @var string + * @var array|null */ - protected $root; - - /** - * @var string - */ - public $subpath = ''; - - /** - * @var string - */ - public $origin = ''; - - /** - * @var array|null - */ - private $info; + private ?array $info = null; /** * @var string[]|null */ - private $dependencies; + private ?array $dependencies = null; /** * Constructs a new Extension object. @@ -87,15 +51,15 @@ class Extension * @param string $pathname * The relative path and filename of the extension's info file; e.g., * 'core/modules/node/node.info.yml'. - * @param string $filename + * @param string|null $filename * (optional) The filename of the main extension file; e.g., 'node.module'. */ - public function __construct($root, $type, $pathname, $filename = null) - { - $this->root = $root; - $this->type = $type; - $this->pathname = $pathname; - $this->filename = $filename; + public function __construct( + protected string $root, + protected string $type, + protected string $pathname, + protected ?string $filename = null + ) { } /** @@ -212,7 +176,7 @@ public function getDependencies(): array // @see \Drupal\Core\Extension\Dependency::createFromString(). foreach ($dependencies as $dependency) { - if (strpos($dependency, ':') !== false) { + if (str_contains($dependency, ':')) { [, $dependency] = explode(':', $dependency); } @@ -234,6 +198,11 @@ private function parseInfo(): array throw new RuntimeException(sprintf('Cannot read "%s"', $this->getPathname())); } - return $this->info = Yaml::parse($infoContent); + $parsed = Yaml::parse($infoContent); + if (!is_array($parsed)) { + throw new RuntimeException(sprintf('Malformed info file "%s"', $this->getPathname())); + } + + return $this->info = $parsed; } } diff --git a/src/Drupal/ExtensionDiscovery.php b/src/Drupal/ExtensionDiscovery.php index 504f18d1..91eafc15 100644 --- a/src/Drupal/ExtensionDiscovery.php +++ b/src/Drupal/ExtensionDiscovery.php @@ -3,7 +3,6 @@ namespace mglaman\PHPStanDrupal\Drupal; use FilesystemIterator; -use mglaman\PHPStanDrupal\Drupal\Extension; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use Symfony\Component\Finder\Finder; @@ -14,8 +13,15 @@ use function file_exists; use function is_dir; use function preg_match; -use function strpos; - +use function str_starts_with; + +/** + * Discovers extensions in a Drupal site. + * + * Bundled version of \Drupal\Core\Extension\ExtensionDiscovery. + * + * @internal + */ class ExtensionDiscovery { @@ -61,23 +67,21 @@ class ExtensionDiscovery /** * List of installation profile directories to additionally scan. * - * @var array + * @var array */ - protected $profileDirectories; + protected array $profileDirectories; /** * The app root for the current operation. - * - * @var string */ - protected $root; + protected string $root; /** * The site paths. * * @var string[] */ - protected $sitePaths; + protected array $sitePaths; /** * Constructs a new ExtensionDiscovery object. @@ -85,7 +89,7 @@ class ExtensionDiscovery * @param string $root * The app root. */ - public function __construct($root) + public function __construct(string $root) { $this->root = $root; $this->profileDirectories = [ @@ -247,13 +251,13 @@ protected function filterByProfileDirectories(array $all_files) } return array_filter($all_files, function (Extension $file) : bool { - if (strpos($file->subpath, 'profiles') !== 0) { + if (!str_starts_with($file->subpath, 'profiles')) { // This extension doesn't belong to a profile, ignore it. return true; } foreach ($this->profileDirectories as $weight => $profile_path) { - if (strpos($file->getPath(), $profile_path) === 0) { + if (str_starts_with($file->getPath(), $profile_path)) { // Parent profile found. return true; } @@ -281,7 +285,7 @@ protected function sort(array $all_files, array $weights) foreach ($all_files as $key => $file) { // If the extension does not belong to a profile, just apply the weight // of the originating directory. - if (strpos($file->subpath, 'profiles') !== 0) { + if (!str_starts_with($file->subpath, 'profiles')) { $origins[$key] = $weights[$file->origin]; $profiles[$key] = null; } elseif ($this->profileDirectories === []) { @@ -293,7 +297,7 @@ protected function sort(array $all_files, array $weights) } else { // Apply the weight of the originating profile directory. foreach ($this->profileDirectories as $weight => $profile_path) { - if (strpos($file->getPath(), $profile_path) === 0) { + if (str_starts_with($file->getPath(), $profile_path)) { $origins[$key] = self::ORIGIN_PROFILE; $profiles[$key] = $weight; continue 2; diff --git a/src/Drupal/RecursiveExtensionFilterIterator.php b/src/Drupal/RecursiveExtensionFilterIterator.php index 86e593c7..9e38a7ef 100644 --- a/src/Drupal/RecursiveExtensionFilterIterator.php +++ b/src/Drupal/RecursiveExtensionFilterIterator.php @@ -1,4 +1,4 @@ - */ - protected $whitelist = [ + protected array $whitelist = [ 'profiles', 'modules', 'themes', @@ -39,9 +39,9 @@ class RecursiveExtensionFilterIterator extends RecursiveFilterIterator * i.e., extensions (of all types) are not able to use any of these names, * because their directory names will be skipped. * - * @var array + * @var list */ - protected $blacklist = [ + protected array $blacklist = [ // Object-oriented code subdirectories. 'src', 'lib', @@ -67,7 +67,7 @@ class RecursiveExtensionFilterIterator extends RecursiveFilterIterator * * @param \RecursiveIterator $iterator * The iterator to filter. - * @param array $blacklist + * @param list $blacklist * (optional) Add to the blacklist of directories that should be filtered * out during the iteration. */ @@ -108,7 +108,7 @@ public function accept(): bool if ($name[0] === '.') { return false; } - if ($this->isDir()) { + if ($this->current()->isDir()) { // If this is a subdirectory of a base search path, only recurse into the // fixed list of expected extension type directory names. Required for // scanning the top-level/root directory; without this condition, we would @@ -126,13 +126,13 @@ public function accept(): bool // config module to be overridden/replaced in a profile/site directory // (whereas it must be located directly in a modules directory). if ($name === 'config') { - return substr($this->current()->getPathname(), -14) === 'modules/config'; + return str_ends_with($this->current()->getPathname(), 'modules/config'); } // Accept the directory unless the name is blacklisted. return !in_array($name, $this->blacklist, true); } // Only accept extension info files. - return substr($name, -9) === '.info.yml'; + return str_ends_with($name, '.info.yml'); } } diff --git a/src/Reflection/EntityFieldMethodsViaMagicReflectionExtension.php b/src/Reflection/EntityFieldMethodsViaMagicReflectionExtension.php index 79fad651..90c5116c 100644 --- a/src/Reflection/EntityFieldMethodsViaMagicReflectionExtension.php +++ b/src/Reflection/EntityFieldMethodsViaMagicReflectionExtension.php @@ -1,4 +1,4 @@ -declaringClass = $declaringClass; - $this->propertyName = $propertyName; - $this->reflectionProvider = $reflectionProvider; + public function __construct( + private readonly ClassReflection $declaringClass, + private readonly string $propertyName, + private readonly ReflectionProvider $reflectionProvider + ) { } public function getReadableType(): Type diff --git a/src/Reflection/EntityFieldsViaMagicReflectionExtension.php b/src/Reflection/EntityFieldsViaMagicReflectionExtension.php index 22a837c8..c1157cd4 100644 --- a/src/Reflection/EntityFieldsViaMagicReflectionExtension.php +++ b/src/Reflection/EntityFieldsViaMagicReflectionExtension.php @@ -1,4 +1,4 @@ -reflectionProvider = $reflectionProvider; + public function __construct( + private readonly ReflectionProvider $reflectionProvider + ) { } public function hasProperty(ClassReflection $classReflection, string $propertyName): bool @@ -32,11 +30,12 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa // @todo Have this run after PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension // We should not have to check for the property tags if we could get this to run after PHPStan's // existing annotation property reflection. - if ($classReflection->hasNativeProperty($propertyName) || array_key_exists($propertyName, $classReflection->getPropertyTags())) { + if ($classReflection->hasNativeProperty($propertyName)) { // Let other parts of PHPStan handle this. return false; } + // A class is its own ancestor, so this also covers the class itself. foreach ($classReflection->getAncestors() as $ancestor) { if (array_key_exists($propertyName, $ancestor->getPropertyTags())) { return false; diff --git a/src/Reflection/FieldItemListMethodReflection.php b/src/Reflection/FieldItemListMethodReflection.php index 69802552..f4c742e1 100644 --- a/src/Reflection/FieldItemListMethodReflection.php +++ b/src/Reflection/FieldItemListMethodReflection.php @@ -1,4 +1,4 @@ -declaringClass = $declaringClass; - $this->propertyName = $propertyName; + public function __construct( + private readonly ClassReflection $declaringClass, + private readonly string $propertyName + ) { } public static function canHandleProperty(ClassReflection $classReflection, string $propertyName): bool diff --git a/src/Rules/Classes/PluginManagerInspectionRule.php b/src/Rules/Classes/PluginManagerInspectionRule.php index 4981faa8..cb4aa0e9 100644 --- a/src/Rules/Classes/PluginManagerInspectionRule.php +++ b/src/Rules/Classes/PluginManagerInspectionRule.php @@ -18,7 +18,7 @@ /** * @implements Rule */ -class PluginManagerInspectionRule implements Rule +final class PluginManagerInspectionRule implements Rule { public function getNodeType(): string { diff --git a/src/Rules/Deprecations/DeprecatedHookImplementation.php b/src/Rules/Deprecations/DeprecatedHookImplementation.php index c14be6a8..72e0013f 100644 --- a/src/Rules/Deprecations/DeprecatedHookImplementation.php +++ b/src/Rules/Deprecations/DeprecatedHookImplementation.php @@ -1,4 +1,4 @@ - */ -class DiscouragedFunctionsRule implements Rule +final class DiscouragedFunctionsRule implements Rule { + private const DISCOURAGED_FUNCTIONS = [ + // Devel module debugging functions. + 'dargs', + 'dcp', + 'dd', + 'dfb', + 'dfbt', + 'dpm', + 'dpq', + 'dpr', + 'dprint_r', + 'drupal_debug', + 'dsm', + 'dvm', + 'dvr', + 'kdevel_print_object', + 'kpr', + 'kprint_r', + 'sdpm', + // Functions which are not available on all + // PHP builds. + 'fnmatch', + ]; + public function getNodeType(): string { return FuncCall::class; @@ -30,31 +54,7 @@ public function processNode(Node $node, Scope $scope): array } $name = strtolower((string)$node->name); - $discouragedFunctions = [ - // Devel module debugging functions. - 'dargs', - 'dcp', - 'dd', - 'dfb', - 'dfbt', - 'dpm', - 'dpq', - 'dpr', - 'dprint_r', - 'drupal_debug', - 'dsm', - 'dvm', - 'dvr', - 'kdevel_print_object', - 'kpr', - 'kprint_r', - 'sdpm', - // Functions which are not available on all - // PHP builds. - 'fnmatch', - ]; - - if (in_array($name, $discouragedFunctions, true)) { + if (in_array($name, self::DISCOURAGED_FUNCTIONS, true)) { return [ RuleErrorBuilder::message( sprintf('Calls to function %s should not exist.', $name) diff --git a/src/Rules/Drupal/GlobalDrupalDependencyInjectionRule.php b/src/Rules/Drupal/GlobalDrupalDependencyInjectionRule.php index a5e0b6aa..1d389bbd 100644 --- a/src/Rules/Drupal/GlobalDrupalDependencyInjectionRule.php +++ b/src/Rules/Drupal/GlobalDrupalDependencyInjectionRule.php @@ -11,8 +11,27 @@ /** * @implements Rule */ -class GlobalDrupalDependencyInjectionRule implements Rule +final class GlobalDrupalDependencyInjectionRule implements Rule { + /** + * Interfaces whose implementations cannot use dependency injection. + */ + private const ALLOWED_INTERFACES = [ + // Ignore tests. + 'PHPUnit\Framework\Test', + // Typed data objects cannot use dependency injection. + 'Drupal\Core\TypedData\TypedDataInterface', + // Entities don't use services for now + // @see https://www.drupal.org/project/drupal/issues/2913224 + 'Drupal\Core\Entity\EntityInterface', + // Stream wrappers are only registered as a service for their tags + // and cannot use dependency injection. Function calls like + // file_exists, stat, etc. will construct the class directly. + 'Drupal\Core\StreamWrapper\StreamWrapperInterface', + // Ignore Nightwatch test setup classes. + 'Drupal\TestSite\TestSetupInterface', + ]; + public function getNodeType(): string { return Node\Expr\StaticCall::class; @@ -35,23 +54,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $allowed_list = [ - // Ignore tests. - 'PHPUnit\Framework\Test', - // Typed data objects cannot use dependency injection. - 'Drupal\Core\TypedData\TypedDataInterface', - // Entities don't use services for now - // @see https://www.drupal.org/project/drupal/issues/2913224 - 'Drupal\Core\Entity\EntityInterface', - // Stream wrappers are only registered as a service for their tags - // and cannot use dependency injection. Function calls like - // file_exists, stat, etc. will construct the class directly. - 'Drupal\Core\StreamWrapper\StreamWrapperInterface', - // Ignore Nightwatch test setup classes. - 'Drupal\TestSite\TestSetupInterface', - ]; - - foreach ($allowed_list as $item) { + foreach (self::ALLOWED_INTERFACES as $item) { if ($scopeClassReflection->implementsInterface($item)) { return []; } diff --git a/src/Rules/Drupal/LoadIncludeBase.php b/src/Rules/Drupal/LoadIncludeBase.php index 208c74d2..8a7c25f3 100644 --- a/src/Rules/Drupal/LoadIncludeBase.php +++ b/src/Rules/Drupal/LoadIncludeBase.php @@ -15,14 +15,9 @@ abstract class LoadIncludeBase implements Rule { - /** - * @var \mglaman\PHPStanDrupal\Drupal\ExtensionMap - */ - protected $extensionMap; - - public function __construct(ExtensionMap $extensionMap) - { - $this->extensionMap = $extensionMap; + public function __construct( + protected readonly ExtensionMap $extensionMap + ) { } private function getStringArgValue(Node\Expr $expr, Scope $scope): ?string diff --git a/src/Rules/Drupal/LoadIncludes.php b/src/Rules/Drupal/LoadIncludes.php index eb48e47c..8367f54b 100644 --- a/src/Rules/Drupal/LoadIncludes.php +++ b/src/Rules/Drupal/LoadIncludes.php @@ -15,7 +15,7 @@ /** * @extends LoadIncludeBase */ -class LoadIncludes extends LoadIncludeBase +final class LoadIncludes extends LoadIncludeBase { public function getNodeType(): string diff --git a/src/Rules/Drupal/ModuleLoadInclude.php b/src/Rules/Drupal/ModuleLoadInclude.php index 7f70831d..3c2dfcb6 100644 --- a/src/Rules/Drupal/ModuleLoadInclude.php +++ b/src/Rules/Drupal/ModuleLoadInclude.php @@ -19,7 +19,7 @@ * * @extends LoadIncludeBase */ -class ModuleLoadInclude extends LoadIncludeBase +final class ModuleLoadInclude extends LoadIncludeBase { public function getNodeType(): string diff --git a/src/Rules/Drupal/PluginManager/AbstractPluginManagerRule.php b/src/Rules/Drupal/PluginManager/AbstractPluginManagerRule.php index 00e34aa5..af8415bb 100644 --- a/src/Rules/Drupal/PluginManager/AbstractPluginManagerRule.php +++ b/src/Rules/Drupal/PluginManager/AbstractPluginManagerRule.php @@ -2,6 +2,7 @@ namespace mglaman\PHPStanDrupal\Rules\Drupal\PluginManager; +use Drupal\Component\Plugin\PluginManagerInterface; use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\Rule; @@ -17,6 +18,6 @@ protected function isPluginManager(ClassReflection $classReflection): bool return !$classReflection->isInterface() && !$classReflection->isAnonymous() && - $classReflection->implementsInterface('Drupal\Component\Plugin\PluginManagerInterface'); + $classReflection->implementsInterface(PluginManagerInterface::class); } } diff --git a/src/Rules/Drupal/PluginManager/PluginManagerSetsCacheBackendRule.php b/src/Rules/Drupal/PluginManager/PluginManagerSetsCacheBackendRule.php index d828167b..712d6044 100644 --- a/src/Rules/Drupal/PluginManager/PluginManagerSetsCacheBackendRule.php +++ b/src/Rules/Drupal/PluginManager/PluginManagerSetsCacheBackendRule.php @@ -11,7 +11,7 @@ /** * @extends AbstractPluginManagerRule */ -class PluginManagerSetsCacheBackendRule extends AbstractPluginManagerRule +final class PluginManagerSetsCacheBackendRule extends AbstractPluginManagerRule { public function getNodeType(): string { diff --git a/src/Type/EntityTypeManagerGetStorageDynamicReturnTypeExtension.php b/src/Type/EntityTypeManagerGetStorageDynamicReturnTypeExtension.php index b1b881da..b4c4b3a4 100644 --- a/src/Type/EntityTypeManagerGetStorageDynamicReturnTypeExtension.php +++ b/src/Type/EntityTypeManagerGetStorageDynamicReturnTypeExtension.php @@ -2,6 +2,7 @@ namespace mglaman\PHPStanDrupal\Type; +use Drupal\Core\Entity\EntityTypeManagerInterface; use mglaman\PHPStanDrupal\Drupal\EntityDataRepository; use mglaman\PHPStanDrupal\Type\EntityStorage\EntityStorageType; use PhpParser\Node\Expr\BinaryOp\Concat; @@ -15,24 +16,14 @@ class EntityTypeManagerGetStorageDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** - * @var EntityDataRepository - */ - private $entityDataRepository; - - /** - * EntityTypeManagerGetStorageDynamicReturnTypeExtension constructor. - * - * @param EntityDataRepository $entityDataRepository - */ - public function __construct(EntityDataRepository $entityDataRepository) - { - $this->entityDataRepository = $entityDataRepository; + public function __construct( + private readonly EntityDataRepository $entityDataRepository + ) { } public function getClass(): string { - return 'Drupal\Core\Entity\EntityTypeManagerInterface'; + return EntityTypeManagerInterface::class; } public function isMethodSupported(MethodReflection $methodReflection): bool