Skip to content

Commit 7a263de

Browse files
committed
Introduce AccessPropertiesCheck
1 parent 06d592d commit 7a263de

7 files changed

+187
-160
lines changed

conf/config.level0.neon

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,6 @@ services:
179179
class: PHPStan\Rules\Properties\AccessPropertiesRule
180180
tags:
181181
- phpstan.rules.rule
182-
arguments:
183-
reportMagicProperties: %reportMagicProperties%
184-
checkDynamicProperties: %checkDynamicProperties%
185182

186183
-
187184
class: PHPStan\Rules\Properties\AccessStaticPropertiesRule

conf/config.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,12 @@ services:
10331033
-
10341034
class: PHPStan\Rules\Playground\NeverRuleHelper
10351035

1036+
-
1037+
class: PHPStan\Rules\Properties\AccessPropertiesCheck
1038+
arguments:
1039+
reportMagicProperties: %reportMagicProperties%
1040+
checkDynamicProperties: %checkDynamicProperties%
1041+
10361042
-
10371043
class: PHPStan\Rules\Properties\LazyReadWritePropertiesExtensionProvider
10381044

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use PhpParser\Node\Expr;
6+
use PhpParser\Node\Expr\PropertyFetch;
7+
use PhpParser\Node\Identifier;
8+
use PHPStan\Analyser\NullsafeOperatorHelper;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Internal\SprintfHelper;
11+
use PHPStan\Reflection\ReflectionProvider;
12+
use PHPStan\Rules\IdentifierRuleError;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use PHPStan\Rules\RuleLevelHelper;
15+
use PHPStan\Type\Constant\ConstantStringType;
16+
use PHPStan\Type\ErrorType;
17+
use PHPStan\Type\StaticType;
18+
use PHPStan\Type\Type;
19+
use PHPStan\Type\VerbosityLevel;
20+
use function array_map;
21+
use function array_merge;
22+
use function count;
23+
use function sprintf;
24+
25+
final class AccessPropertiesCheck
26+
{
27+
28+
public function __construct(
29+
private ReflectionProvider $reflectionProvider,
30+
private RuleLevelHelper $ruleLevelHelper,
31+
private bool $reportMagicProperties,
32+
private bool $checkDynamicProperties,
33+
)
34+
{
35+
}
36+
37+
/**
38+
* @return list<IdentifierRuleError>
39+
*/
40+
public function check(PropertyFetch $node, Scope $scope): array
41+
{
42+
if ($node->name instanceof Identifier) {
43+
$names = [$node->name->name];
44+
} else {
45+
$names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings());
46+
}
47+
48+
$errors = [];
49+
foreach ($names as $name) {
50+
$errors = array_merge($errors, $this->processSingleProperty($scope, $node, $name));
51+
}
52+
53+
return $errors;
54+
}
55+
56+
/**
57+
* @return list<IdentifierRuleError>
58+
*/
59+
private function processSingleProperty(Scope $scope, PropertyFetch $node, string $name): array
60+
{
61+
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
62+
$scope,
63+
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var),
64+
sprintf('Access to property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)),
65+
static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(),
66+
);
67+
$type = $typeResult->getType();
68+
if ($type instanceof ErrorType) {
69+
return $typeResult->getUnknownClassErrors();
70+
}
71+
72+
if ($scope->isInExpressionAssign($node)) {
73+
return [];
74+
}
75+
76+
$typeForDescribe = $type;
77+
if ($type instanceof StaticType) {
78+
$typeForDescribe = $type->getStaticObjectType();
79+
}
80+
81+
if ($type->canAccessProperties()->no() || $type->canAccessProperties()->maybe() && !$scope->isUndefinedExpressionAllowed($node)) {
82+
return [
83+
RuleErrorBuilder::message(sprintf(
84+
'Cannot access property $%s on %s.',
85+
$name,
86+
$typeForDescribe->describe(VerbosityLevel::typeOnly()),
87+
))->identifier('property.nonObject')->build(),
88+
];
89+
}
90+
91+
$has = $type->hasProperty($name);
92+
if (!$has->no() && $this->canAccessUndefinedProperties($scope, $node)) {
93+
return [];
94+
}
95+
96+
if (!$has->yes()) {
97+
if ($scope->hasExpressionType($node)->yes()) {
98+
return [];
99+
}
100+
101+
$classNames = $type->getObjectClassNames();
102+
if (!$this->reportMagicProperties) {
103+
foreach ($classNames as $className) {
104+
if (!$this->reflectionProvider->hasClass($className)) {
105+
continue;
106+
}
107+
108+
$classReflection = $this->reflectionProvider->getClass($className);
109+
if (
110+
$classReflection->hasNativeMethod('__get')
111+
|| $classReflection->hasNativeMethod('__set')
112+
) {
113+
return [];
114+
}
115+
}
116+
}
117+
118+
if (count($classNames) === 1) {
119+
$propertyClassReflection = $this->reflectionProvider->getClass($classNames[0]);
120+
$parentClassReflection = $propertyClassReflection->getParentClass();
121+
while ($parentClassReflection !== null) {
122+
if ($parentClassReflection->hasProperty($name)) {
123+
if ($scope->canAccessProperty($parentClassReflection->getProperty($name, $scope))) {
124+
return [];
125+
}
126+
return [
127+
RuleErrorBuilder::message(sprintf(
128+
'Access to private property $%s of parent class %s.',
129+
$name,
130+
$parentClassReflection->getDisplayName(),
131+
))->identifier('property.private')->build(),
132+
];
133+
}
134+
135+
$parentClassReflection = $parentClassReflection->getParentClass();
136+
}
137+
}
138+
139+
$ruleErrorBuilder = RuleErrorBuilder::message(sprintf(
140+
'Access to an undefined property %s::$%s.',
141+
$typeForDescribe->describe(VerbosityLevel::typeOnly()),
142+
$name,
143+
))->identifier('property.notFound');
144+
if ($typeResult->getTip() !== null) {
145+
$ruleErrorBuilder->tip($typeResult->getTip());
146+
} else {
147+
$ruleErrorBuilder->tip('Learn more: <fg=cyan>https://phpstan.org/blog/solving-phpstan-access-to-undefined-property</>');
148+
}
149+
150+
return [
151+
$ruleErrorBuilder->build(),
152+
];
153+
}
154+
155+
$propertyReflection = $type->getProperty($name, $scope);
156+
if (!$scope->canAccessProperty($propertyReflection)) {
157+
return [
158+
RuleErrorBuilder::message(sprintf(
159+
'Access to %s property %s::$%s.',
160+
$propertyReflection->isPrivate() ? 'private' : 'protected',
161+
$type->describe(VerbosityLevel::typeOnly()),
162+
$name,
163+
))->identifier(sprintf('property.%s', $propertyReflection->isPrivate() ? 'private' : 'protected'))->build(),
164+
];
165+
}
166+
167+
return [];
168+
}
169+
170+
private function canAccessUndefinedProperties(Scope $scope, Expr $node): bool
171+
{
172+
return $scope->isUndefinedExpressionAllowed($node) && !$this->checkDynamicProperties;
173+
}
174+
175+
}

src/Rules/Properties/AccessPropertiesInAssignRule.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
final class AccessPropertiesInAssignRule implements Rule
1414
{
1515

16-
public function __construct(private AccessPropertiesRule $accessPropertiesRule)
16+
public function __construct(private AccessPropertiesCheck $check)
1717
{
1818
}
1919

@@ -32,7 +32,7 @@ public function processNode(Node $node, Scope $scope): array
3232
return [];
3333
}
3434

35-
return $this->accessPropertiesRule->processNode($node->getPropertyFetch(), $scope);
35+
return $this->check->check($node->getPropertyFetch(), $scope);
3636
}
3737

3838
}

src/Rules/Properties/AccessPropertiesRule.php

Lines changed: 2 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,16 @@
44

55
use PhpParser\Node;
66
use PhpParser\Node\Expr\PropertyFetch;
7-
use PhpParser\Node\Identifier;
8-
use PHPStan\Analyser\NullsafeOperatorHelper;
97
use PHPStan\Analyser\Scope;
10-
use PHPStan\Internal\SprintfHelper;
11-
use PHPStan\Reflection\ReflectionProvider;
12-
use PHPStan\Rules\IdentifierRuleError;
138
use PHPStan\Rules\Rule;
14-
use PHPStan\Rules\RuleErrorBuilder;
15-
use PHPStan\Rules\RuleLevelHelper;
16-
use PHPStan\Type\Constant\ConstantStringType;
17-
use PHPStan\Type\ErrorType;
18-
use PHPStan\Type\StaticType;
19-
use PHPStan\Type\Type;
20-
use PHPStan\Type\VerbosityLevel;
21-
use function array_map;
22-
use function array_merge;
23-
use function count;
24-
use function sprintf;
259

2610
/**
2711
* @implements Rule<Node\Expr\PropertyFetch>
2812
*/
2913
final class AccessPropertiesRule implements Rule
3014
{
3115

32-
public function __construct(
33-
private ReflectionProvider $reflectionProvider,
34-
private RuleLevelHelper $ruleLevelHelper,
35-
private bool $reportMagicProperties,
36-
private bool $checkDynamicProperties,
37-
)
16+
public function __construct(private AccessPropertiesCheck $check)
3817
{
3918
}
4019

@@ -45,137 +24,7 @@ public function getNodeType(): string
4524

4625
public function processNode(Node $node, Scope $scope): array
4726
{
48-
if ($node->name instanceof Identifier) {
49-
$names = [$node->name->name];
50-
} else {
51-
$names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings());
52-
}
53-
54-
$errors = [];
55-
foreach ($names as $name) {
56-
$errors = array_merge($errors, $this->processSingleProperty($scope, $node, $name));
57-
}
58-
59-
return $errors;
60-
}
61-
62-
/**
63-
* @return list<IdentifierRuleError>
64-
*/
65-
private function processSingleProperty(Scope $scope, PropertyFetch $node, string $name): array
66-
{
67-
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
68-
$scope,
69-
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var),
70-
sprintf('Access to property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)),
71-
static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(),
72-
);
73-
$type = $typeResult->getType();
74-
if ($type instanceof ErrorType) {
75-
return $typeResult->getUnknownClassErrors();
76-
}
77-
78-
if ($scope->isInExpressionAssign($node)) {
79-
return [];
80-
}
81-
82-
$typeForDescribe = $type;
83-
if ($type instanceof StaticType) {
84-
$typeForDescribe = $type->getStaticObjectType();
85-
}
86-
87-
if ($type->canAccessProperties()->no() || $type->canAccessProperties()->maybe() && !$scope->isUndefinedExpressionAllowed($node)) {
88-
return [
89-
RuleErrorBuilder::message(sprintf(
90-
'Cannot access property $%s on %s.',
91-
$name,
92-
$typeForDescribe->describe(VerbosityLevel::typeOnly()),
93-
))->identifier('property.nonObject')->build(),
94-
];
95-
}
96-
97-
$has = $type->hasProperty($name);
98-
if (!$has->no() && $this->canAccessUndefinedProperties($scope, $node)) {
99-
return [];
100-
}
101-
102-
if (!$has->yes()) {
103-
if ($scope->hasExpressionType($node)->yes()) {
104-
return [];
105-
}
106-
107-
$classNames = $type->getObjectClassNames();
108-
if (!$this->reportMagicProperties) {
109-
foreach ($classNames as $className) {
110-
if (!$this->reflectionProvider->hasClass($className)) {
111-
continue;
112-
}
113-
114-
$classReflection = $this->reflectionProvider->getClass($className);
115-
if (
116-
$classReflection->hasNativeMethod('__get')
117-
|| $classReflection->hasNativeMethod('__set')
118-
) {
119-
return [];
120-
}
121-
}
122-
}
123-
124-
if (count($classNames) === 1) {
125-
$propertyClassReflection = $this->reflectionProvider->getClass($classNames[0]);
126-
$parentClassReflection = $propertyClassReflection->getParentClass();
127-
while ($parentClassReflection !== null) {
128-
if ($parentClassReflection->hasProperty($name)) {
129-
if ($scope->canAccessProperty($parentClassReflection->getProperty($name, $scope))) {
130-
return [];
131-
}
132-
return [
133-
RuleErrorBuilder::message(sprintf(
134-
'Access to private property $%s of parent class %s.',
135-
$name,
136-
$parentClassReflection->getDisplayName(),
137-
))->identifier('property.private')->build(),
138-
];
139-
}
140-
141-
$parentClassReflection = $parentClassReflection->getParentClass();
142-
}
143-
}
144-
145-
$ruleErrorBuilder = RuleErrorBuilder::message(sprintf(
146-
'Access to an undefined property %s::$%s.',
147-
$typeForDescribe->describe(VerbosityLevel::typeOnly()),
148-
$name,
149-
))->identifier('property.notFound');
150-
if ($typeResult->getTip() !== null) {
151-
$ruleErrorBuilder->tip($typeResult->getTip());
152-
} else {
153-
$ruleErrorBuilder->tip('Learn more: <fg=cyan>https://phpstan.org/blog/solving-phpstan-access-to-undefined-property</>');
154-
}
155-
156-
return [
157-
$ruleErrorBuilder->build(),
158-
];
159-
}
160-
161-
$propertyReflection = $type->getProperty($name, $scope);
162-
if (!$scope->canAccessProperty($propertyReflection)) {
163-
return [
164-
RuleErrorBuilder::message(sprintf(
165-
'Access to %s property %s::$%s.',
166-
$propertyReflection->isPrivate() ? 'private' : 'protected',
167-
$type->describe(VerbosityLevel::typeOnly()),
168-
$name,
169-
))->identifier(sprintf('property.%s', $propertyReflection->isPrivate() ? 'private' : 'protected'))->build(),
170-
];
171-
}
172-
173-
return [];
174-
}
175-
176-
private function canAccessUndefinedProperties(Scope $scope, Node\Expr $node): bool
177-
{
178-
return $scope->isUndefinedExpressionAllowed($node) && !$this->checkDynamicProperties;
27+
return $this->check->check($node, $scope);
17928
}
18029

18130
}

0 commit comments

Comments
 (0)