Skip to content

Commit 391fcb8

Browse files
committed
Detect when a class extends a class that has been flagged as @internal
1 parent 337bc36 commit 391fcb8

18 files changed

+436
-0
lines changed

extension.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ services:
7676
-
7777
class: mglaman\PHPStanDrupal\Reflection\EntityFieldsViaMagicReflectionExtension
7878
tags: [phpstan.broker.propertiesClassReflectionExtension]
79+
-
80+
class: mglaman\PHPStanDrupal\Rules\Classes\ClassExtendsInternalClassRule
81+
tags: [phpstan.rules.rule]
82+
arguments:
83+
reflectionProvider: @reflectionProvider
7984
-
8085
class: mglaman\PHPStanDrupal\Rules\Drupal\LoadIncludes
8186
tags: [phpstan.rules.rule]

src/Internal/NamespaceCheck.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace mglaman\PHPStanDrupal\Internal;
4+
5+
use PhpParser\Node\Stmt\Class_;
6+
7+
/**
8+
* @internal
9+
*/
10+
final class NamespaceCheck
11+
{
12+
public static function isDrupalNamespace(Class_ $class): bool
13+
{
14+
// @phpstan-ignore-next-line
15+
if (!isset($class->namespacedName)) {
16+
return false;
17+
}
18+
19+
return 'Drupal' === (string) $class->namespacedName->slice(0, 1);
20+
}
21+
22+
public static function isSharedNamespace(Class_ $class): bool
23+
{
24+
if (!isset($class->extends)) {
25+
return false;
26+
}
27+
28+
// @phpstan-ignore-next-line
29+
if (!isset($class->namespacedName)) {
30+
return false;
31+
}
32+
33+
if (!self::isDrupalNamespace($class)) {
34+
return false;
35+
}
36+
37+
return (string) $class->namespacedName->slice(0, 2) === (string) $class->extends->slice(0, 2);
38+
}
39+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace mglaman\PHPStanDrupal\Rules\Classes;
4+
5+
use mglaman\PHPStanDrupal\Internal\NamespaceCheck;
6+
use PhpParser\Node;
7+
use PhpParser\Node\Stmt\Class_;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\ReflectionProvider;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
13+
class ClassExtendsInternalClassRule implements Rule
14+
{
15+
/**
16+
* @var ReflectionProvider
17+
*/
18+
private $reflectionProvider;
19+
20+
public function __construct(ReflectionProvider $reflectionProvider)
21+
{
22+
$this->reflectionProvider = $reflectionProvider;
23+
}
24+
25+
public function getNodeType(): string
26+
{
27+
return Class_::class;
28+
}
29+
30+
public function processNode(Node $node, Scope $scope): array
31+
{
32+
/** @var Class_ $node */
33+
if (!isset($node->extends)) {
34+
return [];
35+
}
36+
37+
$extendedClassName = $node->extends->toString();
38+
if (!$this->reflectionProvider->hasClass($extendedClassName)) {
39+
return [];
40+
}
41+
42+
$extendedClassReflection = $this->reflectionProvider->getClass($extendedClassName);
43+
if (!$extendedClassReflection->isInternal()) {
44+
return [];
45+
}
46+
47+
// @phpstan-ignore-next-line
48+
if (!isset($node->namespacedName)) {
49+
return $this->buildError(null, $extendedClassName);
50+
}
51+
52+
$currentClassName = $node->namespacedName->toString();
53+
54+
if (!NamespaceCheck::isDrupalNamespace($node)) {
55+
return $this->buildError($currentClassName, $extendedClassName);
56+
}
57+
58+
if (NamespaceCheck::isSharedNamespace($node)) {
59+
return [];
60+
}
61+
62+
return $this->buildError($currentClassName, $extendedClassName);
63+
}
64+
65+
private function buildError(?string $currentClassName, string $extendedClassName): array
66+
{
67+
return [
68+
RuleErrorBuilder::message(\sprintf(
69+
'%s extends @internal class %s.',
70+
$currentClassName !== null ? \sprintf('Class %s', $currentClassName) : 'Anonymous class',
71+
$extendedClassName
72+
))->build()
73+
];
74+
}
75+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name: module_with_internal_classes
2+
type: module
3+
core: 8.x
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Drupal\module_with_internal_classes\Foo;
4+
5+
class ExternalClass {
6+
7+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Drupal\module_with_internal_classes\Foo;
4+
5+
/**
6+
* @internal
7+
*/
8+
class InternalClass {
9+
10+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Drupal\phpstan_fixtures\Internal;
4+
5+
final class DoesNotExtendsAnyClass {
6+
7+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Drupal\phpstan_fixtures\Internal;
4+
5+
use Drupal\Core\PHPStanDrupalTests\ExternalClass;
6+
7+
final class ExtendsDrupalCoreExternalClass extends ExternalClass {
8+
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Drupal\phpstan_fixtures\Internal;
4+
5+
use Drupal\Core\InternalClass;
6+
7+
final class ExtendsDrupalCoreInternalClass extends InternalClass {
8+
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Drupal\phpstan_fixtures\Internal;
4+
5+
use Drupal\Core\PHPStanDrupalTests\InternalClass;
6+
7+
final class ExtendsDrupalCorePHPStanDrupalTestsInternalClass extends InternalClass {
8+
9+
}

0 commit comments

Comments
 (0)