Skip to content

Commit a0e754d

Browse files
rvanvelzenondrejmirtes
authored andcommitted
Support treatPhpDocTypesAsCertain for null-safe methods/properties
1 parent 5bcff71 commit a0e754d

4 files changed

Lines changed: 125 additions & 17 deletions

File tree

src/Rules/Methods/NullsafeMethodCallRule.php

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
7+
use PHPStan\DependencyInjection\AutowiredParameter;
78
use PHPStan\DependencyInjection\RegisteredRule;
89
use PHPStan\Rules\Rule;
910
use PHPStan\Rules\RuleErrorBuilder;
@@ -17,24 +18,50 @@
1718
final class NullsafeMethodCallRule implements Rule
1819
{
1920

21+
public function __construct(
22+
#[AutowiredParameter]
23+
private bool $treatPhpDocTypesAsCertain,
24+
#[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')]
25+
private bool $treatPhpDocTypesAsCertainTip,
26+
)
27+
{
28+
}
29+
2030
public function getNodeType(): string
2131
{
2232
return Node\Expr\NullsafeMethodCall::class;
2333
}
2434

2535
public function processNode(Node $node, Scope $scope): array
2636
{
27-
$calledOnType = $scope->getScopeType($node->var);
37+
$calledOnType = $this->treatPhpDocTypesAsCertain ? $scope->getScopeType($node->var) : $scope->getScopeNativeType($node->var);
2838
if (!$calledOnType->isNull()->no()) {
2939
return [];
3040
}
3141

32-
return [
33-
RuleErrorBuilder::message(sprintf('Using nullsafe method call on non-nullable type %s. Use -> instead.', $calledOnType->describe(VerbosityLevel::typeOnly())))
34-
->line($node->name->getStartLine())
35-
->identifier('nullsafe.neverNull')
36-
->build(),
37-
];
42+
$addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder {
43+
if (!$this->treatPhpDocTypesAsCertain || !$this->treatPhpDocTypesAsCertainTip) {
44+
return $ruleErrorBuilder;
45+
}
46+
47+
$calledOnNativeType = $scope->getScopeNativeType($node->var);
48+
if ($calledOnNativeType->isNull()->no()) {
49+
return $ruleErrorBuilder;
50+
}
51+
52+
return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip();
53+
};
54+
55+
$ruleErrorBuilder = $addTip(
56+
RuleErrorBuilder::message(sprintf(
57+
'Using nullsafe method call on non-nullable type %s. Use -> instead.',
58+
$calledOnType->describe(VerbosityLevel::typeOnly()),
59+
)),
60+
)
61+
->line($node->name->getStartLine())
62+
->identifier('nullsafe.neverNull');
63+
64+
return [$ruleErrorBuilder->build()];
3865
}
3966

4067
}

src/Rules/Properties/NullsafePropertyFetchRule.php

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
7+
use PHPStan\DependencyInjection\AutowiredParameter;
78
use PHPStan\DependencyInjection\RegisteredRule;
89
use PHPStan\Rules\Rule;
910
use PHPStan\Rules\RuleErrorBuilder;
@@ -17,7 +18,12 @@
1718
final class NullsafePropertyFetchRule implements Rule
1819
{
1920

20-
public function __construct()
21+
public function __construct(
22+
#[AutowiredParameter]
23+
private bool $treatPhpDocTypesAsCertain,
24+
#[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')]
25+
private bool $treatPhpDocTypesAsCertainTip,
26+
)
2127
{
2228
}
2329

@@ -28,7 +34,7 @@ public function getNodeType(): string
2834

2935
public function processNode(Node $node, Scope $scope): array
3036
{
31-
$calledOnType = $scope->getScopeType($node->var);
37+
$calledOnType = $this->treatPhpDocTypesAsCertain ? $scope->getScopeType($node->var) : $scope->getScopeNativeType($node->var);
3238
if (!$calledOnType->isNull()->no()) {
3339
return [];
3440
}
@@ -37,12 +43,29 @@ public function processNode(Node $node, Scope $scope): array
3743
return [];
3844
}
3945

40-
return [
41-
RuleErrorBuilder::message(sprintf('Using nullsafe property access on non-nullable type %s. Use -> instead.', $calledOnType->describe(VerbosityLevel::typeOnly())))
42-
->line($node->name->getStartLine())
43-
->identifier('nullsafe.neverNull')
44-
->build(),
45-
];
46+
$addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder {
47+
if (!$this->treatPhpDocTypesAsCertain || !$this->treatPhpDocTypesAsCertainTip) {
48+
return $ruleErrorBuilder;
49+
}
50+
51+
$calledOnNativeType = $scope->getScopeNativeType($node->var);
52+
if ($calledOnNativeType->isNull()->no()) {
53+
return $ruleErrorBuilder;
54+
}
55+
56+
return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip();
57+
};
58+
59+
$ruleErrorBuilder = $addTip(
60+
RuleErrorBuilder::message(sprintf(
61+
'Using nullsafe property access on non-nullable type %s. Use -> instead.',
62+
$calledOnType->describe(VerbosityLevel::typeOnly()),
63+
)),
64+
)
65+
->line($node->name->getStartLine())
66+
->identifier('nullsafe.neverNull');
67+
68+
return [$ruleErrorBuilder->build()];
4669
}
4770

4871
}

tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,24 @@
1212
class NullsafeMethodCallRuleTest extends RuleTestCase
1313
{
1414

15+
private bool $treatPhpDocTypesAsCertain;
16+
1517
protected function getRule(): Rule
1618
{
17-
return new NullsafeMethodCallRule();
19+
return new NullsafeMethodCallRule(
20+
$this->treatPhpDocTypesAsCertain,
21+
true,
22+
);
23+
}
24+
25+
protected function shouldTreatPhpDocTypesAsCertain(): bool
26+
{
27+
return $this->treatPhpDocTypesAsCertain;
1828
}
1929

2030
public function testRule(): void
2131
{
32+
$this->treatPhpDocTypesAsCertain = true;
2233
$this->analyse([__DIR__ . '/data/nullsafe-method-call-rule.php'], [
2334
[
2435
'Using nullsafe method call on non-nullable type Exception. Use -> instead.',
@@ -29,54 +40,70 @@ public function testRule(): void
2940

3041
public function testNullsafeVsScalar(): void
3142
{
43+
$this->treatPhpDocTypesAsCertain = true;
3244
$this->analyse([__DIR__ . '/../../Analyser/nsrt/nullsafe-vs-scalar.php'], []);
3345
}
3446

3547
public function testBug8664(): void
3648
{
49+
$this->treatPhpDocTypesAsCertain = true;
3750
$this->analyse([__DIR__ . '/../../Analyser/data/bug-8664.php'], []);
3851
}
3952

4053
public function testBug14150(): void
4154
{
55+
$this->treatPhpDocTypesAsCertain = true;
4256
$this->analyse([__DIR__ . '/data/bug-14150-nullsafe.php'], [
4357
[
4458
'Using nullsafe method call on non-nullable type $this(Bug14150NullsafeMethod\HelloWorld). Use -> instead.',
4559
21,
60+
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
4661
],
4762
]);
4863
}
4964

65+
public function testBug14150WithoutCertainPhpDocTypes(): void
66+
{
67+
$this->treatPhpDocTypesAsCertain = false;
68+
$this->analyse([__DIR__ . '/data/bug-14150-nullsafe.php'], []);
69+
}
70+
5071
#[RequiresPhp('>= 8.0.0')]
5172
public function testBug9293(): void
5273
{
74+
$this->treatPhpDocTypesAsCertain = true;
5375
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9293.php'], []);
5476
}
5577

5678
#[RequiresPhp('>= 8.0.0')]
5779
public function testBug6922b(): void
5880
{
81+
$this->treatPhpDocTypesAsCertain = true;
5982
$this->analyse([__DIR__ . '/data/bug-6922b.php'], []);
6083
}
6184

6285
public function testBug8523(): void
6386
{
87+
$this->treatPhpDocTypesAsCertain = true;
6488
$this->analyse([__DIR__ . '/data/bug-8523.php'], []);
6589
}
6690

6791
public function testBug8523b(): void
6892
{
93+
$this->treatPhpDocTypesAsCertain = true;
6994
$this->analyse([__DIR__ . '/data/bug-8523b.php'], []);
7095
}
7196

7297
public function testBug8523c(): void
7398
{
99+
$this->treatPhpDocTypesAsCertain = true;
74100
$this->analyse([__DIR__ . '/data/bug-8523c.php'], []);
75101
}
76102

77103
#[RequiresPhp('>= 8.1.0')]
78104
public function testBug12222(): void
79105
{
106+
$this->treatPhpDocTypesAsCertain = true;
80107
$this->analyse([__DIR__ . '/data/bug-12222.php'], []);
81108
}
82109

tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,24 @@
1212
class NullsafePropertyFetchRuleTest extends RuleTestCase
1313
{
1414

15+
private bool $treatPhpDocTypesAsCertain;
16+
1517
protected function getRule(): Rule
1618
{
17-
return new NullsafePropertyFetchRule();
19+
return new NullsafePropertyFetchRule(
20+
$this->treatPhpDocTypesAsCertain,
21+
true,
22+
);
23+
}
24+
25+
protected function shouldTreatPhpDocTypesAsCertain(): bool
26+
{
27+
return $this->treatPhpDocTypesAsCertain;
1828
}
1929

2030
public function testRule(): void
2131
{
32+
$this->treatPhpDocTypesAsCertain = true;
2233
$this->analyse([__DIR__ . '/data/nullsafe-property-fetch-rule.php'], [
2334
[
2435
'Using nullsafe property access on non-nullable type Exception. Use -> instead.',
@@ -29,34 +40,40 @@ public function testRule(): void
2940

3041
public function testBug6020(): void
3142
{
43+
$this->treatPhpDocTypesAsCertain = true;
3244
$this->analyse([__DIR__ . '/data/bug-6020.php'], []);
3345
}
3446

3547
#[RequiresPhp('>= 8.0.0')]
3648
public function testBug7109(): void
3749
{
50+
$this->treatPhpDocTypesAsCertain = true;
3851
$this->analyse([__DIR__ . '/data/bug-7109.php'], []);
3952
}
4053

4154
#[RequiresPhp('>= 8.0.0')]
4255
public function testBug5172(): void
4356
{
57+
$this->treatPhpDocTypesAsCertain = true;
4458
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-5172.php'], []);
4559
}
4660

4761
#[RequiresPhp('>= 8.1.0')]
4862
public function testBug7980(): void
4963
{
64+
$this->treatPhpDocTypesAsCertain = true;
5065
$this->analyse([__DIR__ . '/../../Analyser/data/bug-7980.php'], []);
5166
}
5267

5368
public function testBug8517(): void
5469
{
70+
$this->treatPhpDocTypesAsCertain = true;
5571
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8517.php'], []);
5672
}
5773

5874
public function testBug14150(): void
5975
{
76+
$this->treatPhpDocTypesAsCertain = true;
6077
$this->analyse([__DIR__ . '/data/bug-14150-nullsafe.php'], [
6178
[
6279
'Using nullsafe property access on non-nullable type $this(Bug14150NullsafeProperty\HelloWorld). Use -> instead.',
@@ -65,19 +82,33 @@ public function testBug14150(): void
6582
[
6683
'Using nullsafe property access on non-nullable type $this(Bug14150NullsafeProperty\HelloWorld). Use -> instead.',
6784
27,
85+
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
86+
],
87+
]);
88+
}
89+
90+
public function testBug14150WithoutCertainPhpDocTypes(): void
91+
{
92+
$this->treatPhpDocTypesAsCertain = false;
93+
$this->analyse([__DIR__ . '/data/bug-14150-nullsafe.php'], [
94+
[
95+
'Using nullsafe property access on non-nullable type $this(Bug14150NullsafeProperty\HelloWorld). Use -> instead.',
96+
20,
6897
],
6998
]);
7099
}
71100

72101
#[RequiresPhp('>= 8.0.0')]
73102
public function testBug9105(): void
74103
{
104+
$this->treatPhpDocTypesAsCertain = true;
75105
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9105.php'], []);
76106
}
77107

78108
#[RequiresPhp('>= 8.1.0')]
79109
public function testBug6922(): void
80110
{
111+
$this->treatPhpDocTypesAsCertain = true;
81112
$this->analyse([__DIR__ . '/data/bug-6922.php'], []);
82113
}
83114

0 commit comments

Comments
 (0)