Skip to content

Commit 95f54cd

Browse files
committed
RestrictedPropertyUsageExtension
1 parent d2b7e81 commit 95f54cd

18 files changed

+866
-0
lines changed

conf/config.level2.neon

+5
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ rules:
6868
- PHPStan\Rules\Pure\PureMethodRule
6969

7070
conditionalTags:
71+
PHPStan\Rules\InternalTag\RestrictedInternalPropertyUsageExtension:
72+
phpstan.restrictedPropertyUsageExtension: %featureToggles.internalTag%
7173
PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension:
7274
phpstan.restrictedMethodUsageExtension: %featureToggles.internalTag%
7375

@@ -115,5 +117,8 @@ services:
115117
tags:
116118
- phpstan.rules.rule
117119

120+
-
121+
class: PHPStan\Rules\InternalTag\RestrictedInternalPropertyUsageExtension
122+
118123
-
119124
class: PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension

conf/config.neon

+2
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,10 @@ rules:
222222
- PHPStan\Rules\RestrictedUsage\RestrictedFunctionCallableUsageRule
223223
- PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageRule
224224
- PHPStan\Rules\RestrictedUsage\RestrictedMethodCallableUsageRule
225+
- PHPStan\Rules\RestrictedUsage\RestrictedPropertyUsageRule
225226
- PHPStan\Rules\RestrictedUsage\RestrictedStaticMethodUsageRule
226227
- PHPStan\Rules\RestrictedUsage\RestrictedStaticMethodCallableUsageRule
228+
- PHPStan\Rules\RestrictedUsage\RestrictedStaticPropertyUsageRule
227229

228230
conditionalTags:
229231
PHPStan\Rules\Exceptions\MissingCheckedExceptionInFunctionThrowsRule:

phpstan-baseline.neon

+18
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,24 @@ parameters:
204204
count: 1
205205
path: src/Diagnose/PHPStanDiagnoseExtension.php
206206

207+
-
208+
message: '#^Access to property \$id of internal class Symfony\\Polyfill\\Php80\\PhpToken from outside its root namespace Symfony\.$#'
209+
identifier: property.internalClass
210+
count: 1
211+
path: src/Parser/RichParser.php
212+
213+
-
214+
message: '#^Access to property \$line of internal class Symfony\\Polyfill\\Php80\\PhpToken from outside its root namespace Symfony\.$#'
215+
identifier: property.internalClass
216+
count: 4
217+
path: src/Parser/RichParser.php
218+
219+
-
220+
message: '#^Access to property \$text of internal class Symfony\\Polyfill\\Php80\\PhpToken from outside its root namespace Symfony\.$#'
221+
identifier: property.internalClass
222+
count: 3
223+
path: src/Parser/RichParser.php
224+
207225
-
208226
message: '#^Call to function method_exists\(\) with PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocNode and ''getParamOutTypeTagV…'' will always evaluate to true\.$#'
209227
identifier: function.alreadyNarrowedType

src/DependencyInjection/ConditionalTagsExtension.php

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use PHPStan\Rules\RestrictedUsage\RestrictedClassNameUsageExtension;
2929
use PHPStan\Rules\RestrictedUsage\RestrictedFunctionUsageExtension;
3030
use PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageExtension;
31+
use PHPStan\Rules\RestrictedUsage\RestrictedPropertyUsageExtension;
3132
use PHPStan\ShouldNotHappenException;
3233
use function array_reduce;
3334
use function count;
@@ -79,6 +80,7 @@ public function getConfigSchema(): Nette\Schema\Schema
7980
RestrictedMethodUsageExtension::METHOD_EXTENSION_TAG => $bool,
8081
RestrictedClassNameUsageExtension::CLASS_NAME_EXTENSION_TAG => $bool,
8182
RestrictedFunctionUsageExtension::FUNCTION_EXTENSION_TAG => $bool,
83+
RestrictedPropertyUsageExtension::PROPERTY_EXTENSION_TAG => $bool,
8284
])->min(1));
8385
}
8486

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\InternalTag;
4+
5+
use PHPStan\Analyser\Scope;
6+
use PHPStan\Reflection\ExtendedPropertyReflection;
7+
use PHPStan\Rules\RestrictedUsage\RestrictedPropertyUsageExtension;
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 RestrictedInternalPropertyUsageExtension implements RestrictedPropertyUsageExtension
15+
{
16+
17+
public function __construct(private RestrictedInternalUsageHelper $helper)
18+
{
19+
}
20+
21+
public function isRestrictedPropertyUsage(
22+
ExtendedPropertyReflection $propertyReflection,
23+
Scope $scope,
24+
): ?RestrictedUsage
25+
{
26+
$isPropertyInternal = $propertyReflection->isInternal()->yes();
27+
$declaringClass = $propertyReflection->getDeclaringClass();
28+
$isDeclaringClassInternal = $declaringClass->isInternal();
29+
if (!$isPropertyInternal && !$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 (!$isPropertyInternal) {
41+
return RestrictedUsage::create(
42+
sprintf(
43+
'Access to %sproperty $%s of internal %s %s.',
44+
$propertyReflection->isStatic() ? 'static ' : '',
45+
$propertyReflection->getName(),
46+
strtolower($propertyReflection->getDeclaringClass()->getClassTypeDescription()),
47+
$propertyReflection->getDeclaringClass()->getDisplayName(),
48+
),
49+
sprintf(
50+
'%s.internal%s',
51+
$propertyReflection->isStatic() ? 'staticProperty' : 'property',
52+
$propertyReflection->getDeclaringClass()->getClassTypeDescription(),
53+
),
54+
);
55+
}
56+
57+
return RestrictedUsage::create(
58+
sprintf(
59+
'Access to internal %sproperty %s::$%s.',
60+
$propertyReflection->isStatic() ? 'static ' : '',
61+
$propertyReflection->getDeclaringClass()->getDisplayName(),
62+
$propertyReflection->getName(),
63+
),
64+
sprintf('%s.internal', $propertyReflection->isStatic() ? 'staticProperty' : 'property'),
65+
);
66+
}
67+
68+
if (!$isPropertyInternal) {
69+
return RestrictedUsage::create(
70+
sprintf(
71+
'Access to %sproperty $%s of internal %s %s from outside its root namespace %s.',
72+
$propertyReflection->isStatic() ? 'static ' : '',
73+
$propertyReflection->getName(),
74+
strtolower($propertyReflection->getDeclaringClass()->getClassTypeDescription()),
75+
$propertyReflection->getDeclaringClass()->getDisplayName(),
76+
$namespace,
77+
),
78+
sprintf(
79+
'%s.internal%s',
80+
$propertyReflection->isStatic() ? 'staticProperty' : 'property',
81+
$propertyReflection->getDeclaringClass()->getClassTypeDescription(),
82+
),
83+
);
84+
}
85+
86+
return RestrictedUsage::create(
87+
sprintf(
88+
'Access to internal %sproperty %s::$%s from outside its root namespace %s.',
89+
$propertyReflection->isStatic() ? 'static ' : '',
90+
$propertyReflection->getDeclaringClass()->getDisplayName(),
91+
$propertyReflection->getName(),
92+
$namespace,
93+
),
94+
sprintf('%s.internal', $propertyReflection->isStatic() ? 'staticProperty' : 'property'),
95+
);
96+
}
97+
98+
}
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\ExtendedPropertyReflection;
7+
8+
/**
9+
* Extensions implementing this interface are called for each analysed property access.
10+
*
11+
* Extension can decide to create RestrictedUsage object
12+
* with error message & error identifier to be reported for this property access.
13+
*
14+
* Typical usage is to report errors for properties 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.restrictedPropertyUsageExtension
24+
* ```
25+
*
26+
* @api
27+
*/
28+
interface RestrictedPropertyUsageExtension
29+
{
30+
31+
public const PROPERTY_EXTENSION_TAG = 'phpstan.restrictedPropertyUsageExtension';
32+
33+
public function isRestrictedPropertyUsage(
34+
ExtendedPropertyReflection $propertyReflection,
35+
Scope $scope,
36+
): ?RestrictedUsage;
37+
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\RestrictedUsage;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Identifier;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\DependencyInjection\Container;
9+
use PHPStan\Reflection\ReflectionProvider;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
13+
/**
14+
* @implements Rule<Node\Expr\PropertyFetch>
15+
*/
16+
final class RestrictedPropertyUsageRule implements Rule
17+
{
18+
19+
public function __construct(
20+
private Container $container,
21+
private ReflectionProvider $reflectionProvider,
22+
)
23+
{
24+
}
25+
26+
public function getNodeType(): string
27+
{
28+
return Node\Expr\PropertyFetch::class;
29+
}
30+
31+
/**
32+
* @api
33+
*/
34+
public function processNode(Node $node, Scope $scope): array
35+
{
36+
if (!$node->name instanceof Identifier) {
37+
return [];
38+
}
39+
40+
/** @var RestrictedPropertyUsageExtension[] $extensions */
41+
$extensions = $this->container->getServicesByTag(RestrictedPropertyUsageExtension::PROPERTY_EXTENSION_TAG);
42+
if ($extensions === []) {
43+
return [];
44+
}
45+
46+
$propertyName = $node->name->name;
47+
$propertyCalledOnType = $scope->getType($node->var);
48+
$referencedClasses = $propertyCalledOnType->getObjectClassNames();
49+
50+
$errors = [];
51+
52+
foreach ($referencedClasses as $referencedClass) {
53+
if (!$this->reflectionProvider->hasClass($referencedClass)) {
54+
continue;
55+
}
56+
57+
$classReflection = $this->reflectionProvider->getClass($referencedClass);
58+
if (!$classReflection->hasProperty($propertyName)) {
59+
continue;
60+
}
61+
62+
$propertyReflection = $classReflection->getProperty($propertyName, $scope);
63+
foreach ($extensions as $extension) {
64+
$restrictedUsage = $extension->isRestrictedPropertyUsage($propertyReflection, $scope);
65+
if ($restrictedUsage === null) {
66+
continue;
67+
}
68+
69+
$errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage)
70+
->identifier($restrictedUsage->identifier)
71+
->build();
72+
}
73+
}
74+
75+
return $errors;
76+
}
77+
78+
}
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\StaticPropertyFetch>
19+
*/
20+
final class RestrictedStaticPropertyUsageRule 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\StaticPropertyFetch::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 RestrictedPropertyUsageExtension[] $extensions */
46+
$extensions = $this->container->getServicesByTag(RestrictedPropertyUsageExtension::PROPERTY_EXTENSION_TAG);
47+
if ($extensions === []) {
48+
return [];
49+
}
50+
51+
$propertyName = $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->canAccessProperties()->yes() && $type->hasProperty($propertyName)->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->hasProperty($propertyName)) {
79+
continue;
80+
}
81+
82+
$propertyReflection = $classReflection->getProperty($propertyName, $scope);
83+
foreach ($extensions as $extension) {
84+
$restrictedUsage = $extension->isRestrictedPropertyUsage($propertyReflection, $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+
}

0 commit comments

Comments
 (0)