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
4 changes: 2 additions & 2 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ jobs:
- name: "require phpstan-drupal"
run: |
cd ~/drupal
COMPOSER_MEMORY_LIMIT=-1 composer require mglaman/phpstan-drupal "${{ steps.branch_alias.outputs.VERSION_ALIAS }} as 1.2.99" phpstan/extension-installer --with-all-dependencies
COMPOSER_MEMORY_LIMIT=-1 composer require drupal/core-dev "phpstan/phpstan:^2.0" "phpstan/phpstan-phpunit:^2.0" mglaman/phpstan-drupal "${{ steps.branch_alias.outputs.VERSION_ALIAS }} as 2.99.99" phpstan/extension-installer --with-all-dependencies
cp $GITHUB_WORKSPACE/tests/fixtures/config/drupal-phpstan.neon phpstan.neon
- name: "Test core/install.php"
run: |
Expand Down Expand Up @@ -253,7 +253,7 @@ jobs:
- name: "require phpstan-drupal"
run: |
cd ${{ runner.temp }}/drupal
composer require --dev mglaman/phpstan-drupal "${{ steps.branch_alias.outputs.VERSION_ALIAS }} as 1.1.99" --with-all-dependencies
composer require drupal/core-dev "phpstan/phpstan:^2.0" "phpstan/phpstan-phpunit:^2.0" mglaman/phpstan-drupal "${{ steps.branch_alias.outputs.VERSION_ALIAS }} as 2.99.99" phpstan/extension-installer --with-all-dependencies
- name: "Check baseline"
run: |
cd ${{ runner.temp }}/drupal
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/phpstan-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
phpstan:
- '1.11.x-dev'
- '2.0.x-dev'
steps:
- name: "Checkout"
uses: "actions/checkout@v4"
Expand Down
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
],
"require": {
"php": "^8.1",
"phpstan/phpstan": "^1.10.56",
"phpstan/phpstan-deprecation-rules": "^1.1.4",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-deprecation-rules": "^2.0",
"symfony/finder": "^4.2 || ^5.0 || ^6.0 || ^7.0",
"symfony/yaml": "^4.2|| ^5.0 || ^6.0 || ^7.0",
"webflo/drupal-finder": "^1.3.1"
Expand All @@ -22,8 +22,8 @@
"composer/installers": "^1.9",
"drupal/core-recommended": "^10",
"drush/drush": "^10.0 || ^11 || ^12 || ^13@beta",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-strict-rules": "^1.0",
"phpstan/extension-installer": "1.4.3",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^8.5 || ^9 || ^10 || ^11",
"slevomat/coding-standard": "^7.1",
"squizlabs/php_codesniffer": "^3.3",
Expand Down
2 changes: 0 additions & 2 deletions src/Drupal/DrupalAutoloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,6 @@ public function register(Container $container): void
$levels = 3;
}
$drushDir = dirname($reflect->getFileName(), $levels);
/** @var \SplFileInfo $file */
foreach (Finder::create()->files()->name('*.inc')->in($drushDir . '/includes') as $file) {
require_once $file->getPathname();
}
Expand Down Expand Up @@ -254,7 +253,6 @@ class: Drupal\jsonapi\Routing\JsonApiParamEnhancer

protected function loadLegacyIncludes(): void
{
/** @var \SplFileInfo $file */
foreach (Finder::create()->files()->name('*.inc')->in($this->drupalRoot . '/core/includes') as $file) {
require_once $file->getPathname();
}
Expand Down
4 changes: 2 additions & 2 deletions src/Reflection/EntityFieldsViaMagicReflectionExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\PropertiesClassReflectionExtension;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\IsSuperTypeOfResult;
use PHPStan\Type\ObjectType;
use function array_key_exists;

Expand Down Expand Up @@ -63,7 +63,7 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa
throw new LogicException($classReflection->getName() . "::$propertyName should be handled earlier.");
}

public static function classObjectIsSuperOfInterface(string $name, ObjectType $interfaceObject) : TrinaryLogic
public static function classObjectIsSuperOfInterface(string $name, ObjectType $interfaceObject) : IsSuperTypeOfResult
{
return $interfaceObject->isSuperTypeOf(new ObjectType($name));
}
Expand Down
2 changes: 1 addition & 1 deletion src/Reflection/FieldItemListMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public function getPrototype(): ClassMemberReflection
}

/**
* @return \PHPStan\Reflection\ParametersAcceptor[]
* @return list<\PHPStan\Reflection\ParametersAcceptor>
*/
public function getVariants(): array
{
Expand Down
31 changes: 22 additions & 9 deletions src/Rules/Classes/ClassExtendsInternalClassRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,34 +48,47 @@ public function processNode(Node $node, Scope $scope): array
}

if (!isset($node->namespacedName)) {
return [$this->buildError(null, $extendedClassName)->build()];
return $this->buildError(null, $extendedClassName, null);
}

$currentClassName = $node->namespacedName->toString();

if (!NamespaceCheck::isDrupalNamespace($node)) {
return [$this->buildError($currentClassName, $extendedClassName)->build()];
return $this->buildError($currentClassName, $extendedClassName, null);
}

if (NamespaceCheck::isSharedNamespace($node)) {
return [];
}

$errorBuilder = $this->buildError($currentClassName, $extendedClassName);
$tip = null;
if ($extendedClassName === 'Drupal\Core\Entity\ContentEntityDeleteForm') {
$errorBuilder->tip('Extend \Drupal\Core\Entity\ContentEntityConfirmFormBase. See https://www.drupal.org/node/2491057');
$tip = 'Extend \Drupal\Core\Entity\ContentEntityConfirmFormBase. See https://www.drupal.org/node/2491057';
} elseif ((string) $node->extends->slice(0, 2) === 'Drupal\Core') {
$errorBuilder->tip('Read the Drupal core backwards compatibility and internal API policy: https://www.drupal.org/about/core/policies/core-change-policies/drupal-8-and-9-backwards-compatibility-and-internal-api#internal');
$tip = 'Read the Drupal core backwards compatibility and internal API policy: https://www.drupal.org/about/core/policies/core-change-policies/drupal-8-and-9-backwards-compatibility-and-internal-api#internal';
}
return [$errorBuilder->build()];

return $this->buildError(
$currentClassName,
$extendedClassName,
$tip
);
}

private function buildError(?string $currentClassName, string $extendedClassName): RuleErrorBuilder
/**
* @return list<\PHPStan\Rules\IdentifierRuleError>
*/
private function buildError(?string $currentClassName, string $extendedClassName, ?string $tip): array
{
return RuleErrorBuilder::message(sprintf(
$ruleErrorBuilder = RuleErrorBuilder::message(sprintf(
'%s extends @internal class %s.',
$currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class',
$extendedClassName
));
))->identifier('classExtendsInternalClass.classExtendsInternalClass');
if ($tip !== null) {
$ruleErrorBuilder->tip($tip);
}

return [$ruleErrorBuilder->build()];
}
}
17 changes: 14 additions & 3 deletions src/Rules/Classes/PluginManagerInspectionRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ public function processNode(Node $node, Scope $scope): array
$errors[] = RuleErrorBuilder::message(
'Plugin managers should call alterInfo to allow plugin definitions to be altered.'
)
->identifier('plugin.manager.alterInfoMissing')
->tip('For example, to invoke hook_mymodule_data_alter() call alterInfo with "mymodule_data".')
->line($node->getStartLine())
->identifier('pluginManagerInspection.alterInfoMissing')
->build();
Comment on lines -81 to 84
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't review the commit log, but I'm guessing something about the format of the error identifier made PHPStan mad?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, this is a rare occasion where there was actually an identifier and I completely overlooked it.

Not too sure what to do now...

Do we want double-full-stop identifiers?

If we do I'll have to change the other identifiers in this class to the same prefix.

Help! :)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine taking your changes, I was just curious

}

Expand Down Expand Up @@ -110,6 +110,9 @@ private function isYamlDiscovery(Node\Stmt\Class_ $class): bool
return false;
}

/**
* @return list<\PHPStan\Rules\IdentifierRuleError>
*/
private function inspectYamlPluginManager(Node\Stmt\Class_ $class, Node\Stmt\ClassMethod $constructorMethodNode): array
{
$errors = [];
Expand All @@ -119,7 +122,11 @@ private function inspectYamlPluginManager(Node\Stmt\Class_ $class, Node\Stmt\Cla
$constructor = $reflection->getConstructor();

if ($constructor->getDeclaringClass()->getName() !== $fqn) {
$errors[] = sprintf('%s must override __construct if using YAML plugins.', $fqn);
$errors[] = RuleErrorBuilder::message(
sprintf('%s must override __construct if using YAML plugins.', $fqn)
)
->identifier('pluginManagerInspection.callAlterInfo')
->build();
} else {
foreach ($constructorMethodNode->stmts ?? [] as $constructorStmt) {
if ($constructorStmt instanceof Node\Stmt\Expression) {
Expand All @@ -130,7 +137,11 @@ private function inspectYamlPluginManager(Node\Stmt\Class_ $class, Node\Stmt\Cla
&& ((string)$constructorStmt->class === 'parent')
&& $constructorStmt->name instanceof Node\Identifier
&& $constructorStmt->name->name === '__construct') {
$errors[] = 'YAML plugin managers should not invoke its parent constructor.';
$errors[] = RuleErrorBuilder::message(
'YAML plugin managers should not invoke its parent constructor.'
)
->identifier('pluginManagerInspection.yamlPluginManagersInvokesParentConstructor')
->build();
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion src/Rules/Deprecations/AccessDeprecatedConstant.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function array_merge;
use function explode;
use function sprintf;
Expand Down Expand Up @@ -124,7 +125,11 @@ public function processNode(Node $node, Scope $scope): array
$constantName = $this->reflectionProvider->resolveConstantName($node->name, $scope);
if (isset($deprecatedConstants[$constantName])) {
return [
sprintf('Call to deprecated constant %s: %s', $constantName, $deprecatedConstants[$constantName])
RuleErrorBuilder::message(
sprintf('Call to deprecated constant %s: %s', $constantName, $deprecatedConstants[$constantName])
)
->identifier("accessDeprecatedConstant.deprecatedConstantCalled")
->build()
];
}
return [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public function processNode(Node $node, Scope $scope): array
return [
RuleErrorBuilder::message('Passing context values to plugins via configuration is deprecated in drupal:9.1.0 and will be removed before drupal:10.0.0. Instead, call ::setContextValue() on the plugin itself. See https://www.drupal.org/node/3120980')
->line($node->getStartLine())
->identifier("conditionManagerCreateInstanceContextConfiguration.callSetContextValue")
->build()
];
}
Expand Down
8 changes: 7 additions & 1 deletion src/Rules/Deprecations/ConfigEntityConfigExportRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use function preg_match;

Expand All @@ -18,6 +19,9 @@ protected function getExpectedInterface(): string
return 'Drupal\Core\Config\Entity\ConfigEntityInterface';
}

/**
* @return list<\PHPStan\Rules\IdentifierRuleError>
*/
protected function doProcessNode(ClassReflection $reflection, Node\Stmt\Class_ $node, Scope $scope): array
{
$phpDoc = $reflection->getResolvedPhpDoc();
Expand All @@ -32,7 +36,9 @@ protected function doProcessNode(ClassReflection $reflection, Node\Stmt\Class_ $
}
if ($hasMatch === 0) {
return [
'Configuration entity must define a `config_export` key. See https://www.drupal.org/node/2481909',
RuleErrorBuilder::message('Configuration entity must define a `config_export` key. See https://www.drupal.org/node/2481909')
->identifier('configEntityConfigExport.noConfigExportKey')
->build()
];
}
return [];
Expand Down
3 changes: 3 additions & 0 deletions src/Rules/Deprecations/DeprecatedAnnotationsRuleBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public function getNodeType(): string

abstract protected function getExpectedInterface(): string;

/**
* @return list<\PHPStan\Rules\IdentifierRuleError>
*/
abstract protected function doProcessNode(
ClassReflection $reflection,
Node\Stmt\Class_ $node,
Expand Down
15 changes: 12 additions & 3 deletions src/Rules/Deprecations/DeprecatedHookImplementation.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,27 +56,31 @@ public function processNode(Node $node, Scope $scope) : array
return $this->buildError(
$function_name,
$hook_name,
'useHookFieldWidgetSingleElementFormAlter',
'in drupal:9.2.0 and is removed from drupal:10.0.0. Use hook_field_widget_single_element_form_alter instead.'
);
}
if (str_starts_with($hook_name, 'hook_field_widget_') && str_ends_with($hook_name, '_form_alter')) {
return $this->buildError(
$function_name,
'hook_field_widget_WIDGET_TYPE_form_alter',
'useHookFieldWidgetWidgetTypeFormAlter',
'in drupal:9.2.0 and is removed from drupal:10.0.0. Use hook_field_widget_single_element_WIDGET_TYPE_form_alter instead.'
);
}
if ($hook_name === 'hook_field_widget_multivalue_form_alter') {
return $this->buildError(
$function_name,
$hook_name,
'useHookFieldWidgetCompleteFormAlter',
'in drupal:9.2.0 and is removed from drupal:10.0.0. Use hook_field_widget_complete_form_alter instead.'
);
}
if (str_starts_with($hook_name, 'hook_field_widget_multivalue_') && str_ends_with($hook_name, '_form_alter')) {
return $this->buildError(
$function_name,
'hook_field_widget_multivalue_WIDGET_TYPE_form_alter',
'useHookFieldWidgetMulitValueWidgetTypeFormAlter',
'in drupal:9.2.0 and is removed from drupal:10.0.0. Use hook_field_widget_complete_WIDGET_TYPE_form_alter instead.'
);
}
Expand All @@ -89,16 +93,21 @@ public function processNode(Node $node, Scope $scope) : array
return [];
}

return $this->buildError($function_name, $hook_name, $reflection->getDeprecatedDescription());
return $this->buildError($function_name, $hook_name, 'other', $reflection->getDeprecatedDescription());
}

private function buildError(string $function_name, string $hook_name, ?string $deprecated_description): array
/**
* @return list<\PHPStan\Rules\IdentifierRuleError>
*/
private function buildError(string $function_name, string $hook_name, string $identifier, ?string $deprecated_description): array
{
$deprecated_description = $deprecated_description !== null ? " $deprecated_description" : ".";
return [
RuleErrorBuilder::message(
"Function $function_name implements $hook_name which is deprecated$deprecated_description",
)->build()
)
->identifier("deprecatedHookImplementation.{$identifier}")
->build()
];
}
}
7 changes: 6 additions & 1 deletion src/Rules/Deprecations/GetDeprecatedServiceRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* @implements Rule<Node\Expr\MethodCall>
Expand Down Expand Up @@ -57,7 +58,11 @@ public function processNode(Node $node, Scope $scope): array

$service = $this->serviceMap->getService($serviceName->value);
if (($service instanceof DrupalServiceDefinition) && $service->isDeprecated()) {
return [$service->getDeprecatedDescription()];
return [
RuleErrorBuilder::message($service->getDeprecatedDescription())
->identifier('getDeprecatedService.deprecated')
->build(),
];
}

return [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use function preg_match;

Expand All @@ -16,6 +17,9 @@ protected function getExpectedInterface(): string
return 'Drupal\Component\Plugin\ContextAwarePluginInterface';
}

/**
* @return list<\PHPStan\Rules\IdentifierRuleError>
*/
protected function doProcessNode(ClassReflection $reflection, Node\Stmt\Class_ $node, Scope $scope): array
{
$annotation = $reflection->getResolvedPhpDoc();
Expand All @@ -30,7 +34,11 @@ protected function doProcessNode(ClassReflection $reflection, Node\Stmt\Class_ $
}
if ($hasMatch === 1) {
return [
'Providing context definitions via the "context" key is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.0. Use the "context_definitions" key instead.',
RuleErrorBuilder::message(
'Providing context definitions via the "context" key is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.0. Use the "context_definitions" key instead.',
)
->identifier('pluginAnnotationContextDefinitions.useContextDefinitionsKey')
->build()
];
}
return [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* @implements Rule<Node\Expr\StaticCall>
Expand Down Expand Up @@ -66,7 +67,11 @@ public function processNode(Node $node, Scope $scope): array

$service = $this->serviceMap->getService($serviceName->value);
if (($service instanceof DrupalServiceDefinition) && $service->isDeprecated()) {
return [$service->getDeprecatedDescription()];
return [
RuleErrorBuilder::message($service->getDeprecatedDescription())
->identifier('staticServiceDeprecatedService.deprecated')
->build(),
];
}

return [];
Expand Down
Loading
Loading