Skip to content

Commit a1e697b

Browse files
schlndhondrejmirtes
authored andcommitted
Bleeding edge: stricter ++/-- operator check
1 parent 04f8636 commit a1e697b

File tree

6 files changed

+266
-15
lines changed

6 files changed

+266
-15
lines changed

conf/config.level0.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ services:
205205
tags:
206206
- phpstan.rules.rule
207207
arguments:
208+
bleedingEdge: %featureToggles.bleedingEdge%
208209
checkThisOnly: %checkThisOnly%
209210

210211
-

src/Rules/Operators/InvalidIncDecOperationRule.php

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,17 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Rules\Rule;
88
use PHPStan\Rules\RuleErrorBuilder;
9+
use PHPStan\Rules\RuleLevelHelper;
910
use PHPStan\ShouldNotHappenException;
11+
use PHPStan\Type\BooleanType;
1012
use PHPStan\Type\ErrorType;
13+
use PHPStan\Type\FloatType;
14+
use PHPStan\Type\IntegerType;
15+
use PHPStan\Type\NullType;
16+
use PHPStan\Type\ObjectType;
17+
use PHPStan\Type\StringType;
18+
use PHPStan\Type\Type;
19+
use PHPStan\Type\UnionType;
1120
use PHPStan\Type\VerbosityLevel;
1221
use function get_class;
1322
use function sprintf;
@@ -18,7 +27,11 @@
1827
class InvalidIncDecOperationRule implements Rule
1928
{
2029

21-
public function __construct(private bool $checkThisOnly)
30+
public function __construct(
31+
private RuleLevelHelper $ruleLevelHelper,
32+
private bool $bleedingEdge,
33+
private bool $checkThisOnly,
34+
)
2235
{
2336
}
2437

@@ -74,28 +87,42 @@ public function processNode(Node $node, Scope $scope): array
7487
];
7588
}
7689

77-
if (!$this->checkThisOnly) {
90+
if (!$this->bleedingEdge) {
91+
if ($this->checkThisOnly) {
92+
return [];
93+
}
94+
7895
$varType = $scope->getType($node->var);
7996
if (!$varType->toString() instanceof ErrorType) {
8097
return [];
8198
}
8299
if (!$varType->toNumber() instanceof ErrorType) {
83100
return [];
84101
}
102+
} else {
103+
$allowedTypes = new UnionType([new BooleanType(), new FloatType(), new IntegerType(), new StringType(), new NullType(), new ObjectType('SimpleXMLElement')]);
104+
$varType = $this->ruleLevelHelper->findTypeToCheck(
105+
$scope,
106+
$node->var,
107+
'',
108+
static fn (Type $type): bool => $allowedTypes->isSuperTypeOf($type)->yes(),
109+
)->getType();
85110

86-
return [
87-
RuleErrorBuilder::message(sprintf(
88-
'Cannot use %s on %s.',
89-
$operatorString,
90-
$varType->describe(VerbosityLevel::value()),
91-
))
92-
->line($node->var->getStartLine())
93-
->identifier(sprintf('%s.type', $nodeType))
94-
->build(),
95-
];
111+
if ($varType instanceof ErrorType || $allowedTypes->isSuperTypeOf($varType)->yes()) {
112+
return [];
113+
}
96114
}
97115

98-
return [];
116+
return [
117+
RuleErrorBuilder::message(sprintf(
118+
'Cannot use %s on %s.',
119+
$operatorString,
120+
$varType->describe(VerbosityLevel::value()),
121+
))
122+
->line($node->var->getStartLine())
123+
->identifier(sprintf('%s.type', $nodeType))
124+
->build(),
125+
];
99126
}
100127

101128
}

tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Rules\Operators;
44

55
use PHPStan\Rules\Rule;
6+
use PHPStan\Rules\RuleLevelHelper;
67
use PHPStan\Testing\RuleTestCase;
78

89
/**
@@ -11,9 +12,17 @@
1112
class InvalidIncDecOperationRuleTest extends RuleTestCase
1213
{
1314

15+
private bool $checkExplicitMixed = false;
16+
17+
private bool $checkImplicitMixed = false;
18+
1419
protected function getRule(): Rule
1520
{
16-
return new InvalidIncDecOperationRule(false);
21+
return new InvalidIncDecOperationRule(
22+
new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false),
23+
true,
24+
false,
25+
);
1726
}
1827

1928
public function testRule(): void
@@ -31,6 +40,108 @@ public function testRule(): void
3140
'Cannot use ++ on stdClass.',
3241
17,
3342
],
43+
[
44+
'Cannot use ++ on InvalidIncDec\\ClassWithToString.',
45+
19,
46+
],
47+
[
48+
'Cannot use -- on InvalidIncDec\\ClassWithToString.',
49+
21,
50+
],
51+
[
52+
'Cannot use ++ on array{}.',
53+
23,
54+
],
55+
[
56+
'Cannot use -- on array{}.',
57+
25,
58+
],
59+
[
60+
'Cannot use ++ on resource.',
61+
28,
62+
],
63+
[
64+
'Cannot use -- on resource.',
65+
32,
66+
],
67+
]);
68+
}
69+
70+
public function testMixed(): void
71+
{
72+
$this->checkExplicitMixed = true;
73+
$this->checkImplicitMixed = true;
74+
$this->analyse([__DIR__ . '/data/invalid-inc-dec-mixed.php'], [
75+
[
76+
'Cannot use ++ on T of mixed.',
77+
12,
78+
],
79+
[
80+
'Cannot use ++ on T of mixed.',
81+
14,
82+
],
83+
[
84+
'Cannot use -- on T of mixed.',
85+
16,
86+
],
87+
[
88+
'Cannot use -- on T of mixed.',
89+
18,
90+
],
91+
[
92+
'Cannot use ++ on mixed.',
93+
24,
94+
],
95+
[
96+
'Cannot use ++ on mixed.',
97+
26,
98+
],
99+
[
100+
'Cannot use -- on mixed.',
101+
28,
102+
],
103+
[
104+
'Cannot use -- on mixed.',
105+
30,
106+
],
107+
[
108+
'Cannot use ++ on mixed.',
109+
36,
110+
],
111+
[
112+
'Cannot use ++ on mixed.',
113+
38,
114+
],
115+
[
116+
'Cannot use -- on mixed.',
117+
40,
118+
],
119+
[
120+
'Cannot use -- on mixed.',
121+
42,
122+
],
123+
]);
124+
}
125+
126+
public function testUnion(): void
127+
{
128+
$this->analyse([__DIR__ . '/data/invalid-inc-dec-union.php'], [
129+
[
130+
'Cannot use ++ on array|bool|float|int|object|string|null.',
131+
24,
132+
],
133+
[
134+
'Cannot use -- on array|bool|float|int|object|string|null.',
135+
26,
136+
],
137+
[
138+
'Cannot use ++ on (array|object).',
139+
29,
140+
],
141+
[
142+
'Cannot use -- on (array|object).',
143+
31,
144+
],
34145
]);
35146
}
36147

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace InvalidIncDecMixed;
4+
5+
/**
6+
* @template T
7+
* @param T $a
8+
*/
9+
function genericMixed(mixed $a): void
10+
{
11+
$b = $a;
12+
var_dump(++$b);
13+
$b = $a;
14+
var_dump($b++);
15+
$b = $a;
16+
var_dump(--$b);
17+
$b = $a;
18+
var_dump($b--);
19+
}
20+
21+
function explicitMixed(mixed $a): void
22+
{
23+
$b = $a;
24+
var_dump(++$b);
25+
$b = $a;
26+
var_dump($b++);
27+
$b = $a;
28+
var_dump(--$b);
29+
$b = $a;
30+
var_dump($b--);
31+
}
32+
33+
function implicitMixed($a): void
34+
{
35+
$b = $a;
36+
var_dump(++$b);
37+
$b = $a;
38+
var_dump($b++);
39+
$b = $a;
40+
var_dump(--$b);
41+
$b = $a;
42+
var_dump($b--);
43+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace InvalidIncDecUnion;
4+
5+
/**
6+
* @param __benevolent<scalar|null|array|object> $benevolentUnion
7+
* @param string|int|float|bool|null $okUnion
8+
* @param scalar|null|array|object $union
9+
* @param __benevolent<array|object> $badBenevolentUnion
10+
*/
11+
function foo($benevolentUnion, $okUnion, $union, $badBenevolentUnion): void
12+
{
13+
$a = $benevolentUnion;
14+
$a++;
15+
$a = $benevolentUnion;
16+
--$a;
17+
18+
$a = $okUnion;
19+
$a++;
20+
$a = $okUnion;
21+
--$a;
22+
23+
$a = $union;
24+
$a++;
25+
$a = $union;
26+
--$a;
27+
28+
$a = $badBenevolentUnion;
29+
$a++;
30+
$a = $badBenevolentUnion;
31+
--$a;
32+
}

tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace InvalidIncDec;
44

5-
function ($a, int $i, ?float $j, string $str, \stdClass $std) {
5+
function ($a, int $i, ?float $j, string $str, \stdClass $std, \SimpleXMLElement $simpleXMLElement) {
66
$a++;
77

88
$b = [1];
@@ -15,4 +15,41 @@ function ($a, int $i, ?float $j, string $str, \stdClass $std) {
1515
$j++;
1616
$str++;
1717
$std++;
18+
$classWithToString = new ClassWithToString();
19+
$classWithToString++;
20+
$classWithToString = new ClassWithToString();
21+
--$classWithToString;
22+
$arr = [];
23+
$arr++;
24+
$arr = [];
25+
--$arr;
26+
27+
if (($f = fopen('php://stdin', 'r')) !== false) {
28+
$f++;
29+
}
30+
31+
if (($f = fopen('php://stdin', 'r')) !== false) {
32+
--$f;
33+
}
34+
35+
$bool = true;
36+
$bool++;
37+
$bool = false;
38+
--$bool;
39+
$null = null;
40+
$null++;
41+
$null = null;
42+
--$null;
43+
$a = $simpleXMLElement;
44+
$a++;
45+
$a = $simpleXMLElement;
46+
--$a;
1847
};
48+
49+
class ClassWithToString
50+
{
51+
public function __toString(): string
52+
{
53+
return 'foo';
54+
}
55+
}

0 commit comments

Comments
 (0)