Skip to content
1 change: 1 addition & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ rules:
- mglaman\PHPStanDrupal\Rules\Drupal\LoadIncludes
- mglaman\PHPStanDrupal\Rules\Drupal\EntityQuery\EntityQueryHasAccessCheckRule
- mglaman\PHPStanDrupal\Rules\Drupal\TestClassesProtectedPropertyModulesRule
- mglaman\PHPStanDrupal\Rules\Drupal\CacheableDependencyRule

conditionalTags:
mglaman\PHPStanDrupal\Rules\Drupal\Tests\TestClassSuffixNameRule:
Expand Down
52 changes: 52 additions & 0 deletions src/Rules/Drupal/CacheableDependencyRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace mglaman\PHPStanDrupal\Rules\Drupal;

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 [];
}

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

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

$interfaceType = new ObjectType('Drupal\Core\Cache\CacheableDependencyInterface');
$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(),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?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);
}
}
47 changes: 47 additions & 0 deletions tests/src/Rules/CachableDependencyRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?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.',
39
],
[
'Calling addCacheableDependency($object) when $object does not implement CacheableDependencyInterface effectively disables caching and should be avoided.',
43
],
]
];
}


}
46 changes: 46 additions & 0 deletions tests/src/Rules/data/cacheable-dependency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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);

// This should NOT trigger an error - CacheableMetadata implements CacheableDependencyInterface
$correct_dependency = new CacheableMetadata();
$cacheable_metadata->addCacheableDependency($correct_dependency);
}
}

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

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

// Incorrect usage - should trigger error
$object = new \StdClass;
$cacheable_metadata->addCacheableDependency($object);

// Another incorrect usage - should trigger error
$another_object = new \DateTime();
$cacheable_metadata->addCacheableDependency($another_object);
}
}

Loading