Skip to content

Commit ecab490

Browse files
committed
Introduce AccessStaticPropertiesCheck
1 parent 2599f46 commit ecab490

File tree

5 files changed

+272
-244
lines changed

5 files changed

+272
-244
lines changed
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\StaticPropertyFetch;
7+
use PhpParser\Node\Name;
8+
use PHPStan\Analyser\NullsafeOperatorHelper;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\DependencyInjection\AutowiredParameter;
11+
use PHPStan\DependencyInjection\AutowiredService;
12+
use PHPStan\Internal\SprintfHelper;
13+
use PHPStan\Reflection\ReflectionProvider;
14+
use PHPStan\Rules\ClassNameCheck;
15+
use PHPStan\Rules\ClassNameNodePair;
16+
use PHPStan\Rules\ClassNameUsageLocation;
17+
use PHPStan\Rules\IdentifierRuleError;
18+
use PHPStan\Rules\RuleErrorBuilder;
19+
use PHPStan\Rules\RuleLevelHelper;
20+
use PHPStan\Type\Constant\ConstantStringType;
21+
use PHPStan\Type\ErrorType;
22+
use PHPStan\Type\StringType;
23+
use PHPStan\Type\ThisType;
24+
use PHPStan\Type\Type;
25+
use PHPStan\Type\TypeCombinator;
26+
use PHPStan\Type\TypeUtils;
27+
use PHPStan\Type\VerbosityLevel;
28+
use function array_map;
29+
use function array_merge;
30+
use function count;
31+
use function in_array;
32+
use function sprintf;
33+
use function strtolower;
34+
35+
#[AutowiredService]
36+
final class AccessStaticPropertiesCheck
37+
{
38+
39+
public function __construct(
40+
private ReflectionProvider $reflectionProvider,
41+
private RuleLevelHelper $ruleLevelHelper,
42+
private ClassNameCheck $classCheck,
43+
#[AutowiredParameter(ref: '%tips.discoveringSymbols%')]
44+
private bool $discoveringSymbolsTip,
45+
)
46+
{
47+
}
48+
49+
/**
50+
* @return list<IdentifierRuleError>
51+
*/
52+
public function check(StaticPropertyFetch $node, Scope $scope): array
53+
{
54+
if ($node->name instanceof Node\VarLikeIdentifier) {
55+
$names = [$node->name->name];
56+
} else {
57+
$names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings());
58+
}
59+
60+
$errors = [];
61+
foreach ($names as $name) {
62+
$errors = array_merge($errors, $this->processSingleProperty($scope, $node, $name));
63+
}
64+
65+
return $errors;
66+
}
67+
68+
/**
69+
* @return list<IdentifierRuleError>
70+
*/
71+
private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, string $name): array
72+
{
73+
$messages = [];
74+
if ($node->class instanceof Name) {
75+
$class = (string) $node->class;
76+
$lowercasedClass = strtolower($class);
77+
if (in_array($lowercasedClass, ['self', 'static'], true)) {
78+
if (!$scope->isInClass()) {
79+
return [
80+
RuleErrorBuilder::message(sprintf(
81+
'Accessing %s::$%s outside of class scope.',
82+
$class,
83+
$name,
84+
))->identifier(sprintf('outOfClass.%s', $lowercasedClass))->build(),
85+
];
86+
}
87+
$classType = $scope->resolveTypeByName($node->class);
88+
} elseif ($lowercasedClass === 'parent') {
89+
if (!$scope->isInClass()) {
90+
return [
91+
RuleErrorBuilder::message(sprintf(
92+
'Accessing %s::$%s outside of class scope.',
93+
$class,
94+
$name,
95+
))->identifier('outOfClass.parent')->build(),
96+
];
97+
}
98+
if ($scope->getClassReflection()->getParentClass() === null) {
99+
return [
100+
RuleErrorBuilder::message(sprintf(
101+
'%s::%s() accesses parent::$%s but %s does not extend any class.',
102+
$scope->getClassReflection()->getDisplayName(),
103+
$scope->getFunctionName(),
104+
$name,
105+
$scope->getClassReflection()->getDisplayName(),
106+
))->identifier('class.noParent')->build(),
107+
];
108+
}
109+
110+
$classType = $scope->resolveTypeByName($node->class);
111+
} else {
112+
if (!$this->reflectionProvider->hasClass($class)) {
113+
if ($scope->isInClassExists($class)) {
114+
return [];
115+
}
116+
117+
$errorBuilder = RuleErrorBuilder::message(sprintf(
118+
'Access to static property $%s on an unknown class %s.',
119+
$name,
120+
$class,
121+
))
122+
->identifier('class.notFound');
123+
124+
if ($this->discoveringSymbolsTip) {
125+
$errorBuilder->discoveringSymbolsTip();
126+
}
127+
128+
return [
129+
$errorBuilder->build(),
130+
];
131+
}
132+
133+
$locationData = [];
134+
$locationClassReflection = $this->reflectionProvider->getClass($class);
135+
if ($locationClassReflection->hasStaticProperty($name)) {
136+
$locationData['property'] = $locationClassReflection->getStaticProperty($name);
137+
}
138+
139+
$messages = $this->classCheck->checkClassNames(
140+
$scope,
141+
[new ClassNameNodePair($class, $node->class)],
142+
ClassNameUsageLocation::from(ClassNameUsageLocation::STATIC_PROPERTY_ACCESS, $locationData),
143+
);
144+
145+
$classType = $scope->resolveTypeByName($node->class);
146+
}
147+
} else {
148+
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
149+
$scope,
150+
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->class),
151+
sprintf('Access to static property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)),
152+
static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasStaticProperty($name)->yes(),
153+
);
154+
$classType = $classTypeResult->getType();
155+
if ($classType instanceof ErrorType) {
156+
return $classTypeResult->getUnknownClassErrors();
157+
}
158+
}
159+
160+
if ($classType->isString()->yes()) {
161+
return [];
162+
}
163+
164+
$typeForDescribe = $classType;
165+
if ($classType instanceof ThisType) {
166+
$typeForDescribe = $classType->getStaticObjectType();
167+
}
168+
$classType = TypeCombinator::remove($classType, new StringType());
169+
170+
if ($scope->isInExpressionAssign($node)) {
171+
return [];
172+
}
173+
174+
if ($classType->canAccessProperties()->no() || $classType->canAccessProperties()->maybe() && !$scope->isUndefinedExpressionAllowed($node)) {
175+
return array_merge($messages, [
176+
RuleErrorBuilder::message(sprintf(
177+
'Cannot access static property $%s on %s.',
178+
$name,
179+
$typeForDescribe->describe(VerbosityLevel::typeOnly()),
180+
))->identifier('staticProperty.nonObject')->build(),
181+
]);
182+
}
183+
184+
$has = $classType->hasStaticProperty($name);
185+
if (!$has->no() && $scope->isUndefinedExpressionAllowed($node)) {
186+
return [];
187+
}
188+
189+
if (!$has->yes()) {
190+
if ($scope->hasExpressionType($node)->yes()) {
191+
return $messages;
192+
}
193+
194+
$classNames = $classType->getObjectClassNames();
195+
if (count($classNames) === 1) {
196+
$propertyClassReflection = $this->reflectionProvider->getClass($classNames[0]);
197+
$parentClassReflection = $propertyClassReflection->getParentClass();
198+
199+
while ($parentClassReflection !== null) {
200+
if ($parentClassReflection->hasStaticProperty($name)) {
201+
if ($scope->canReadProperty($parentClassReflection->getStaticProperty($name))) {
202+
return [];
203+
}
204+
return [
205+
RuleErrorBuilder::message(sprintf(
206+
'Access to private static property $%s of parent class %s.',
207+
$name,
208+
$parentClassReflection->getDisplayName(),
209+
))->identifier('staticProperty.private')->build(),
210+
];
211+
}
212+
213+
$parentClassReflection = $parentClassReflection->getParentClass();
214+
}
215+
}
216+
217+
if ($classType->hasInstanceProperty($name)->yes()) {
218+
$hasPropertyTypes = TypeUtils::getHasPropertyTypes($classType);
219+
foreach ($hasPropertyTypes as $hasPropertyType) {
220+
if ($hasPropertyType->getPropertyName() === $name) {
221+
return [];
222+
}
223+
}
224+
225+
return array_merge($messages, [
226+
RuleErrorBuilder::message(sprintf(
227+
'Static access to instance property %s::$%s.',
228+
$classType->getInstanceProperty($name, $scope)->getDeclaringClass()->getDisplayName(),
229+
$name,
230+
))->identifier('property.staticAccess')->build(),
231+
]);
232+
}
233+
234+
return array_merge($messages, [
235+
RuleErrorBuilder::message(sprintf(
236+
'Access to an undefined static property %s::$%s.',
237+
$typeForDescribe->describe(VerbosityLevel::typeOnly()),
238+
$name,
239+
))->identifier('staticProperty.notFound')->build(),
240+
]);
241+
}
242+
243+
$property = $classType->getStaticProperty($name, $scope);
244+
if (!$scope->canReadProperty($property)) {
245+
return array_merge($messages, [
246+
RuleErrorBuilder::message(sprintf(
247+
'Access to %s property $%s of class %s.',
248+
$property->isPrivate() ? 'private' : 'protected',
249+
$name,
250+
$property->getDeclaringClass()->getDisplayName(),
251+
))->identifier(sprintf('staticProperty.%s', $property->isPrivate() ? 'private' : 'protected'))->build(),
252+
]);
253+
}
254+
255+
return $messages;
256+
}
257+
258+
}

src/Rules/Properties/AccessStaticPropertiesInAssignRule.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
final class AccessStaticPropertiesInAssignRule implements Rule
1616
{
1717

18-
public function __construct(private AccessStaticPropertiesRule $accessStaticPropertiesRule)
18+
public function __construct(private AccessStaticPropertiesCheck $check)
1919
{
2020
}
2121

@@ -34,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array
3434
return [];
3535
}
3636

37-
return $this->accessStaticPropertiesRule->processNode($node->getPropertyFetch(), $scope);
37+
return $this->check->check($node->getPropertyFetch(), $scope);
3838
}
3939

4040
}

0 commit comments

Comments
 (0)