Skip to content

Commit e14e527

Browse files
committed
RestrictedClassConstantUsageExtension
1 parent 95f54cd commit e14e527

13 files changed

+538
-0
lines changed

conf/config.level0.neon

+5
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ rules:
104104
- PHPStan\Rules\Whitespace\FileWhitespaceRule
105105

106106
conditionalTags:
107+
PHPStan\Rules\InternalTag\RestrictedInternalClassConstantUsageExtension:
108+
phpstan.restrictedClassConstantUsageExtension: %featureToggles.internalTag%
107109
PHPStan\Rules\InternalTag\RestrictedInternalClassNameUsageExtension:
108110
phpstan.restrictedClassNameUsageExtension: %featureToggles.internalTag%
109111
PHPStan\Rules\InternalTag\RestrictedInternalFunctionUsageExtension:
@@ -297,6 +299,9 @@ services:
297299
tags:
298300
- phpstan.rules.rule
299301

302+
-
303+
class: PHPStan\Rules\InternalTag\RestrictedInternalClassConstantUsageExtension
304+
300305
-
301306
class: PHPStan\Rules\InternalTag\RestrictedInternalClassNameUsageExtension
302307

conf/config.neon

+1
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ rules:
218218
- PHPStan\Rules\Debug\DumpPhpDocTypeRule
219219
- PHPStan\Rules\Debug\DumpTypeRule
220220
- PHPStan\Rules\Debug\FileAssertRule
221+
- PHPStan\Rules\RestrictedUsage\RestrictedClassConstantUsageRule
221222
- PHPStan\Rules\RestrictedUsage\RestrictedFunctionUsageRule
222223
- PHPStan\Rules\RestrictedUsage\RestrictedFunctionCallableUsageRule
223224
- PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageRule

src/DependencyInjection/ConditionalTagsExtension.php

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtensionProvider;
2626
use PHPStan\Rules\LazyRegistry;
2727
use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
28+
use PHPStan\Rules\RestrictedUsage\RestrictedClassConstantUsageExtension;
2829
use PHPStan\Rules\RestrictedUsage\RestrictedClassNameUsageExtension;
2930
use PHPStan\Rules\RestrictedUsage\RestrictedFunctionUsageExtension;
3031
use PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageExtension;
@@ -81,6 +82,7 @@ public function getConfigSchema(): Nette\Schema\Schema
8182
RestrictedClassNameUsageExtension::CLASS_NAME_EXTENSION_TAG => $bool,
8283
RestrictedFunctionUsageExtension::FUNCTION_EXTENSION_TAG => $bool,
8384
RestrictedPropertyUsageExtension::PROPERTY_EXTENSION_TAG => $bool,
85+
RestrictedClassConstantUsageExtension::CLASS_CONSTANT_EXTENSION_TAG => $bool,
8486
])->min(1));
8587
}
8688

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\InternalTag;
4+
5+
use PHPStan\Analyser\Scope;
6+
use PHPStan\Reflection\ClassConstantReflection;
7+
use PHPStan\Rules\RestrictedUsage\RestrictedClassConstantUsageExtension;
8+
use PHPStan\Rules\RestrictedUsage\RestrictedUsage;
9+
use function array_slice;
10+
use function explode;
11+
use function sprintf;
12+
use function strtolower;
13+
14+
final class RestrictedInternalClassConstantUsageExtension implements RestrictedClassConstantUsageExtension
15+
{
16+
17+
public function __construct(private RestrictedInternalUsageHelper $helper)
18+
{
19+
}
20+
21+
public function isRestrictedClassConstantUsage(
22+
ClassConstantReflection $constantReflection,
23+
Scope $scope,
24+
): ?RestrictedUsage
25+
{
26+
$isConstantInternal = $constantReflection->isInternal()->yes();
27+
$declaringClass = $constantReflection->getDeclaringClass();
28+
$isDeclaringClassInternal = $declaringClass->isInternal();
29+
if (!$isConstantInternal && !$isDeclaringClassInternal) {
30+
return null;
31+
}
32+
33+
$declaringClassName = $declaringClass->getName();
34+
if (!$this->helper->shouldBeReported($scope, $declaringClassName)) {
35+
return null;
36+
}
37+
38+
$namespace = array_slice(explode('\\', $declaringClassName), 0, -1)[0] ?? null;
39+
if ($namespace === null) {
40+
if (!$isConstantInternal) {
41+
return RestrictedUsage::create(
42+
sprintf(
43+
'Access to constant %s of internal %s %s.',
44+
$constantReflection->getName(),
45+
strtolower($constantReflection->getDeclaringClass()->getClassTypeDescription()),
46+
$constantReflection->getDeclaringClass()->getDisplayName(),
47+
),
48+
sprintf(
49+
'classConstant.internal%s',
50+
$constantReflection->getDeclaringClass()->getClassTypeDescription(),
51+
),
52+
);
53+
}
54+
55+
return RestrictedUsage::create(
56+
sprintf(
57+
'Access to internal constant %s::%s.',
58+
$constantReflection->getDeclaringClass()->getDisplayName(),
59+
$constantReflection->getName(),
60+
),
61+
'classConstant.internal',
62+
);
63+
}
64+
65+
if (!$isConstantInternal) {
66+
return RestrictedUsage::create(
67+
sprintf(
68+
'Access to constant %s of internal %s %s from outside its root namespace %s.',
69+
$constantReflection->getName(),
70+
strtolower($constantReflection->getDeclaringClass()->getClassTypeDescription()),
71+
$constantReflection->getDeclaringClass()->getDisplayName(),
72+
$namespace,
73+
),
74+
sprintf(
75+
'classConstant.internal%s',
76+
$constantReflection->getDeclaringClass()->getClassTypeDescription(),
77+
),
78+
);
79+
}
80+
81+
return RestrictedUsage::create(
82+
sprintf(
83+
'Access to constant %s of internal %s %s from outside its root namespace %s.',
84+
$constantReflection->getName(),
85+
strtolower($constantReflection->getDeclaringClass()->getClassTypeDescription()),
86+
$constantReflection->getDeclaringClass()->getDisplayName(),
87+
$namespace,
88+
),
89+
'classConstant.internal',
90+
);
91+
}
92+
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\RestrictedUsage;
4+
5+
use PHPStan\Analyser\Scope;
6+
use PHPStan\Reflection\ClassConstantReflection;
7+
8+
/**
9+
* Extensions implementing this interface are called for each analysed class constant access.
10+
*
11+
* Extension can decide to create RestrictedUsage object
12+
* with error message & error identifier to be reported for this class constant access.
13+
*
14+
* Typical usage is to report errors for constants marked as @-deprecated or @-internal.
15+
*
16+
* To register it in the configuration file use the following tag:
17+
*
18+
* ```
19+
* services:
20+
* -
21+
* class: App\PHPStan\MyExtension
22+
* tags:
23+
* - phpstan.restrictedClassConstantUsageExtension
24+
* ```
25+
*
26+
* @api
27+
*/
28+
interface RestrictedClassConstantUsageExtension
29+
{
30+
31+
public const CLASS_CONSTANT_EXTENSION_TAG = 'phpstan.restrictedClassConstantUsageExtension';
32+
33+
public function isRestrictedClassConstantUsage(
34+
ClassConstantReflection $constantReflection,
35+
Scope $scope,
36+
): ?RestrictedUsage;
37+
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\RestrictedUsage;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Identifier;
7+
use PhpParser\Node\Name;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\DependencyInjection\Container;
10+
use PHPStan\Reflection\ReflectionProvider;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
use PHPStan\Rules\RuleLevelHelper;
14+
use PHPStan\Type\ErrorType;
15+
use PHPStan\Type\Type;
16+
17+
/**
18+
* @implements Rule<Node\Expr\ClassConstFetch>
19+
*/
20+
final class RestrictedClassConstantUsageRule implements Rule
21+
{
22+
23+
public function __construct(
24+
private Container $container,
25+
private ReflectionProvider $reflectionProvider,
26+
private RuleLevelHelper $ruleLevelHelper,
27+
)
28+
{
29+
}
30+
31+
public function getNodeType(): string
32+
{
33+
return Node\Expr\ClassConstFetch::class;
34+
}
35+
36+
/**
37+
* @api
38+
*/
39+
public function processNode(Node $node, Scope $scope): array
40+
{
41+
if (!$node->name instanceof Identifier) {
42+
return [];
43+
}
44+
45+
/** @var RestrictedClassConstantUsageExtension[] $extensions */
46+
$extensions = $this->container->getServicesByTag(RestrictedClassConstantUsageExtension::CLASS_CONSTANT_EXTENSION_TAG);
47+
if ($extensions === []) {
48+
return [];
49+
}
50+
51+
$constantName = $node->name->name;
52+
$referencedClasses = [];
53+
54+
if ($node->class instanceof Name) {
55+
$referencedClasses[] = $scope->resolveName($node->class);
56+
} else {
57+
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
58+
$scope,
59+
$node->class,
60+
'', // We don't care about the error message
61+
static fn (Type $type): bool => $type->canAccessConstants()->yes() && $type->hasConstant($constantName)->yes(),
62+
);
63+
64+
if ($classTypeResult->getType() instanceof ErrorType) {
65+
return [];
66+
}
67+
68+
$referencedClasses = $classTypeResult->getReferencedClasses();
69+
}
70+
71+
$errors = [];
72+
foreach ($referencedClasses as $referencedClass) {
73+
if (!$this->reflectionProvider->hasClass($referencedClass)) {
74+
continue;
75+
}
76+
77+
$classReflection = $this->reflectionProvider->getClass($referencedClass);
78+
if (!$classReflection->hasConstant($constantName)) {
79+
continue;
80+
}
81+
82+
$constantReflection = $classReflection->getConstant($constantName);
83+
foreach ($extensions as $extension) {
84+
$restrictedUsage = $extension->isRestrictedClassConstantUsage($constantReflection, $scope);
85+
if ($restrictedUsage === null) {
86+
continue;
87+
}
88+
89+
$errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage)
90+
->identifier($restrictedUsage->identifier)
91+
->build();
92+
}
93+
}
94+
95+
return $errors;
96+
}
97+
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\InternalTag;
4+
5+
use PHPStan\Rules\RestrictedUsage\RestrictedClassConstantUsageRule;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
9+
/**
10+
* @extends RuleTestCase<RestrictedClassConstantUsageRule>
11+
*/
12+
class RestrictedInternalClassConstantUsageExtensionTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return self::getContainer()->getByType(RestrictedClassConstantUsageRule::class);
18+
}
19+
20+
public function testRule(): void
21+
{
22+
$this->analyse([__DIR__ . '/data/class-constant-internal-tag.php'], [
23+
[
24+
'Access to constant INTERNAL of internal class ClassConstantInternalTagOne\Foo from outside its root namespace ClassConstantInternalTagOne.',
25+
49,
26+
],
27+
[
28+
'Access to constant FOO of internal class ClassConstantInternalTagOne\FooInternal from outside its root namespace ClassConstantInternalTagOne.',
29+
54,
30+
],
31+
[
32+
'Access to constant INTERNAL of internal class ClassConstantInternalTagOne\Foo from outside its root namespace ClassConstantInternalTagOne.',
33+
62,
34+
],
35+
36+
[
37+
'Access to constant FOO of internal class ClassConstantInternalTagOne\FooInternal from outside its root namespace ClassConstantInternalTagOne.',
38+
67,
39+
],
40+
[
41+
'Access to internal constant FooWithInternalClassConstantWithoutNamespace::INTERNAL.',
42+
89,
43+
],
44+
[
45+
'Access to constant FOO of internal class FooInternalWithClassConstantWithoutNamespace.',
46+
94,
47+
],
48+
[
49+
'Access to internal constant FooWithInternalClassConstantWithoutNamespace::INTERNAL.',
50+
102,
51+
],
52+
[
53+
'Access to constant FOO of internal class FooInternalWithClassConstantWithoutNamespace.',
54+
107,
55+
],
56+
]);
57+
}
58+
59+
public function testStaticPropertyAccessOnInternalSubclass(): void
60+
{
61+
$this->analyse([__DIR__ . '/data/class-constant-access-on-internal-subclass.php'], [
62+
[
63+
'Access to constant BAR of internal class ClassConstantAccessOnInternalSubclassOne\Bar from outside its root namespace ClassConstantAccessOnInternalSubclassOne.',
64+
28,
65+
],
66+
]);
67+
}
68+
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace ClassConstantAccessOnInternalSubclassOne {
4+
5+
class Foo
6+
{
7+
public const FOO = 1;
8+
}
9+
10+
/**
11+
* @internal
12+
*/
13+
class Bar extends Foo
14+
{
15+
16+
public const BAR = 1;
17+
18+
}
19+
20+
}
21+
22+
namespace ClassConstantAccessOnInternalSubclassTwo {
23+
24+
use ClassConstantAccessOnInternalSubclassOne\Bar;
25+
26+
function (): void {
27+
Bar::FOO;
28+
Bar::BAR;
29+
};
30+
}

0 commit comments

Comments
 (0)