Skip to content
1 change: 1 addition & 0 deletions bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ parameters:
testClassSuffixNameRule: true
dependencySerializationTraitPropertyRule: true
accessResultConditionRule: true
cacheableDependencyRule: true
2 changes: 2 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ parameters:
accessResultConditionRule: false
classExtendsInternalClassRule: true
pluginManagerInspectionRule: false
cacheableDependencyRule: false
entityMapping:
aggregator_feed:
class: Drupal\aggregator\Entity\Feed
Expand Down Expand Up @@ -267,6 +268,7 @@ parametersSchema:
accessResultConditionRule: boolean()
classExtendsInternalClassRule: boolean()
pluginManagerInspectionRule: boolean()
cacheableDependencyRule: boolean()
])
entityMapping: arrayOf(anyOf(
structure([
Expand Down
4 changes: 4 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ conditionalTags:
phpstan.rules.rule: %drupal.rules.classExtendsInternalClassRule%
mglaman\PHPStanDrupal\Rules\Classes\PluginManagerInspectionRule:
phpstan.rules.rule: %drupal.rules.pluginManagerInspectionRule%
mglaman\PHPStanDrupal\Rules\Drupal\CacheableDependencyRule:
phpstan.rules.rule: %drupal.rules.cacheableDependencyRule%

services:
-
Expand All @@ -38,3 +40,5 @@ services:
class: mglaman\PHPStanDrupal\Rules\Classes\ClassExtendsInternalClassRule
-
class: mglaman\PHPStanDrupal\Rules\Classes\PluginManagerInspectionRule
-
class: mglaman\PHPStanDrupal\Rules\Drupal\CacheableDependencyRule
79 changes: 79 additions & 0 deletions src/Rules/Drupal/CacheableDependencyRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace mglaman\PHPStanDrupal\Rules\Drupal;

use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Plugin\Context\ContextInterface;
use Drupal\Core\Render\RendererInterface;
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ObjectType;

/**
* @implements Rule<Node\Expr\MethodCall>
*/
class CacheableDependencyRule implements Rule
{

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

public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Identifier || $node->name->toString() !== 'addCacheableDependency') {
return [];
}

$receiverType = $scope->getType($node->var);

$allowedInterfaces = [
RefinableCacheableDependencyInterface::class => 0,
CacheableResponseInterface::class => 0,
ContextInterface::class => 0,
RendererInterface::class => 1,
];

$argumentIndex = null;
foreach ($allowedInterfaces as $interfaceName => $argPosition) {
$interfaceType = new ObjectType($interfaceName);
if ($interfaceType->isSuperTypeOf($receiverType)->yes()) {
$argumentIndex = $argPosition;
break;
}
}

if ($argumentIndex === null) {
return [];
}

$args = $node->getArgs();
if (count($args) <= $argumentIndex) {
return [];
}

$dependencyArg = $args[$argumentIndex]->value;
$object = $scope->getType($dependencyArg);

$interfaceType = new ObjectType(CacheableDependencyInterface::class);
$implementsInterface = $interfaceType->isSuperTypeOf($object);

if (!$implementsInterface->no()) {
return [];
}

return [
RuleErrorBuilder::message('Calling addCacheableDependency($object) when $object does not implement CacheableDependencyInterface effectively disables caching and should be avoided.')
->identifier('cacheable.dependency')
->build(),
];
}
}
51 changes: 51 additions & 0 deletions tests/src/Rules/CachableDependencyRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php declare(strict_types=1);

namespace mglaman\PHPStanDrupal\Tests\Rules;

use mglaman\PHPStanDrupal\Rules\Drupal\CacheableDependencyRule;
use mglaman\PHPStanDrupal\Tests\DrupalRuleTestCase;

final class CachableDependencyRuleTest extends DrupalRuleTestCase {

protected function getRule(): \PHPStan\Rules\Rule
{
return new CacheableDependencyRule();
}

/**
* @dataProvider resultData
*
* @param list<array{0: string, 1: int, 2?: string|null}> $errorMessages
*/
public function testRule(string $path, array $errorMessages): void
{
$this->analyse([$path], $errorMessages);
}

public static function resultData(): \Generator
{
yield 'all test cases' => [
__DIR__ . '/data/cacheable-dependency.php',
[
[
'Calling addCacheableDependency($object) when $object does not implement CacheableDependencyInterface effectively disables caching and should be avoided.',
13
],
[
'Calling addCacheableDependency($object) when $object does not implement CacheableDependencyInterface effectively disables caching and should be avoided.',
36
],
[
'Calling addCacheableDependency($object) when $object does not implement CacheableDependencyInterface effectively disables caching and should be avoided.',
39
],
[
'Calling addCacheableDependency($object) when $object does not implement CacheableDependencyInterface effectively disables caching and should be avoided.',
55
],
]
];
}


}
59 changes: 59 additions & 0 deletions tests/src/Rules/data/cacheable-dependency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Drupal\phpstan_fixtures;

use Drupal\Core\Cache\CacheableMetadata;

class UsesIncorrectCacheableDependency {
public function test() {
$element = [];
$cacheable_metadata = CacheableMetadata::createFromRenderArray($element);

$object = new \Stdclass;
$cacheable_metadata->addCacheableDependency($object);
}
}

class UsesCorrectCacheableDependency {
public function test() {
$element = [];
$cacheable_metadata = CacheableMetadata::createFromRenderArray($element);

$correct_dependency = new CacheableMetadata();
$cacheable_metadata->addCacheableDependency($correct_dependency);
}
}

class MultipleCacheableDependencyCalls {
public function test() {
$element = [];
$cacheable_metadata = CacheableMetadata::createFromRenderArray($element);

$correct_dependency = new CacheableMetadata();
$cacheable_metadata->addCacheableDependency($correct_dependency);

$object = new \StdClass;
$cacheable_metadata->addCacheableDependency($object);

$another_object = new \DateTime();
$cacheable_metadata->addCacheableDependency($another_object);
}
}

class RendererInterfaceTestCase {
public function testCorrectUsage(\Drupal\Core\Render\RendererInterface $renderer) {
$elements = [];

$correct_dependency = new CacheableMetadata();
$renderer->addCacheableDependency($elements, $correct_dependency);
}

public function testIncorrectUsage(\Drupal\Core\Render\RendererInterface $renderer) {
$elements = [];

$object = new \StdClass;
$renderer->addCacheableDependency($elements, $object);
}
}


Loading