Skip to content

Commit 8a1f098

Browse files
schlndhondrejmirtes
authored andcommitted
Bleeding edge - check mixed in unary operator
1 parent e35eae4 commit 8a1f098

File tree

6 files changed

+284
-20
lines changed

6 files changed

+284
-20
lines changed

conf/config.level2.neon

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ rules:
3030
- PHPStan\Rules\Generics\UsedTraitsRule
3131
- PHPStan\Rules\Methods\CallPrivateMethodThroughStaticRule
3232
- PHPStan\Rules\Methods\IncompatibleDefaultParameterTypeRule
33-
- PHPStan\Rules\Operators\InvalidUnaryOperationRule
3433
- PHPStan\Rules\Operators\InvalidComparisonOperationRule
3534
- PHPStan\Rules\PhpDoc\FunctionConditionalReturnTypeRule
3635
- PHPStan\Rules\PhpDoc\MethodConditionalReturnTypeRule
@@ -143,3 +142,9 @@ services:
143142
bleedingEdge: %featureToggles.bleedingEdge%
144143
tags:
145144
- phpstan.rules.rule
145+
-
146+
class: PHPStan\Rules\Operators\InvalidUnaryOperationRule
147+
arguments:
148+
bleedingEdge: %featureToggles.bleedingEdge%
149+
tags:
150+
- phpstan.rules.rule

src/Rules/Operators/InvalidUnaryOperationRule.php

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
namespace PHPStan\Rules\Operators;
44

55
use PhpParser\Node;
6+
use PHPStan\Analyser\MutatingScope;
67
use PHPStan\Analyser\Scope;
78
use PHPStan\Rules\Rule;
89
use PHPStan\Rules\RuleErrorBuilder;
10+
use PHPStan\Rules\RuleLevelHelper;
11+
use PHPStan\ShouldNotHappenException;
912
use PHPStan\Type\ErrorType;
13+
use PHPStan\Type\Type;
1014
use PHPStan\Type\VerbosityLevel;
1115
use function sprintf;
1216

@@ -16,6 +20,13 @@
1620
class InvalidUnaryOperationRule implements Rule
1721
{
1822

23+
public function __construct(
24+
private RuleLevelHelper $ruleLevelHelper,
25+
private bool $bleedingEdge,
26+
)
27+
{
28+
}
29+
1930
public function getNodeType(): string
2031
{
2132
return Node\Expr::class;
@@ -31,28 +42,58 @@ public function processNode(Node $node, Scope $scope): array
3142
return [];
3243
}
3344

34-
if ($scope->getType($node) instanceof ErrorType) {
45+
if ($this->bleedingEdge) {
46+
$varName = '__PHPSTAN__LEFT__';
47+
$variable = new Node\Expr\Variable($varName);
48+
$newNode = clone $node;
49+
$newNode->setAttribute('phpstan_cache_printer', null);
50+
$newNode->expr = $variable;
3551

36-
if ($node instanceof Node\Expr\UnaryPlus) {
37-
$operator = '+';
38-
} elseif ($node instanceof Node\Expr\UnaryMinus) {
39-
$operator = '-';
52+
if ($node instanceof Node\Expr\BitwiseNot) {
53+
$callback = static fn (Type $type): bool => $type->isString()->yes() || $type->isInteger()->yes() || $type->isFloat()->yes();
4054
} else {
41-
$operator = '~';
55+
$callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType;
4256
}
43-
return [
44-
RuleErrorBuilder::message(sprintf(
45-
'Unary operation "%s" on %s results in an error.',
46-
$operator,
47-
$scope->getType($node->expr)->describe(VerbosityLevel::value()),
48-
))
49-
->line($node->expr->getStartLine())
50-
->identifier('unaryOp.invalid')
51-
->build(),
52-
];
57+
58+
$exprType = $this->ruleLevelHelper->findTypeToCheck(
59+
$scope,
60+
$node->expr,
61+
'',
62+
$callback,
63+
)->getType();
64+
if ($exprType instanceof ErrorType) {
65+
return [];
66+
}
67+
68+
if (!$scope instanceof MutatingScope) {
69+
throw new ShouldNotHappenException();
70+
}
71+
72+
$scope = $scope->assignVariable($varName, $exprType, $exprType);
73+
if (!$scope->getType($newNode) instanceof ErrorType) {
74+
return [];
75+
}
76+
} elseif (!$scope->getType($node) instanceof ErrorType) {
77+
return [];
5378
}
5479

55-
return [];
80+
if ($node instanceof Node\Expr\UnaryPlus) {
81+
$operator = '+';
82+
} elseif ($node instanceof Node\Expr\UnaryMinus) {
83+
$operator = '-';
84+
} else {
85+
$operator = '~';
86+
}
87+
return [
88+
RuleErrorBuilder::message(sprintf(
89+
'Unary operation "%s" on %s results in an error.',
90+
$operator,
91+
$scope->getType($node->expr)->describe(VerbosityLevel::value()),
92+
))
93+
->line($node->expr->getStartLine())
94+
->identifier('unaryOp.invalid')
95+
->build(),
96+
];
5697
}
5798

5899
}

tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php

Lines changed: 127 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,16 @@
1112
class InvalidUnaryOperationRuleTest 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 InvalidUnaryOperationRule();
21+
return new InvalidUnaryOperationRule(
22+
new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false),
23+
true,
24+
);
1725
}
1826

1927
public function testRule(): void
@@ -39,6 +47,124 @@ public function testRule(): void
3947
'Unary operation "~" on array{} results in an error.',
4048
24,
4149
],
50+
[
51+
'Unary operation "~" on bool results in an error.',
52+
36,
53+
],
54+
[
55+
'Unary operation "+" on array results in an error.',
56+
38,
57+
],
58+
[
59+
'Unary operation "-" on array results in an error.',
60+
39,
61+
],
62+
[
63+
'Unary operation "~" on array results in an error.',
64+
40,
65+
],
66+
[
67+
'Unary operation "+" on object results in an error.',
68+
42,
69+
],
70+
[
71+
'Unary operation "-" on object results in an error.',
72+
43,
73+
],
74+
[
75+
'Unary operation "~" on object results in an error.',
76+
44,
77+
],
78+
[
79+
'Unary operation "+" on resource results in an error.',
80+
50,
81+
],
82+
[
83+
'Unary operation "-" on resource results in an error.',
84+
51,
85+
],
86+
[
87+
'Unary operation "~" on resource results in an error.',
88+
52,
89+
],
90+
[
91+
'Unary operation "~" on null results in an error.',
92+
61,
93+
],
94+
]);
95+
}
96+
97+
public function testMixed(): void
98+
{
99+
$this->checkImplicitMixed = true;
100+
$this->checkExplicitMixed = true;
101+
$this->analyse([__DIR__ . '/data/invalid-unary-mixed.php'], [
102+
[
103+
'Unary operation "+" on T results in an error.',
104+
11,
105+
],
106+
[
107+
'Unary operation "-" on T results in an error.',
108+
12,
109+
],
110+
[
111+
'Unary operation "~" on T results in an error.',
112+
13,
113+
],
114+
[
115+
'Unary operation "+" on mixed results in an error.',
116+
18,
117+
],
118+
[
119+
'Unary operation "-" on mixed results in an error.',
120+
19,
121+
],
122+
[
123+
'Unary operation "~" on mixed results in an error.',
124+
20,
125+
],
126+
[
127+
'Unary operation "+" on mixed results in an error.',
128+
25,
129+
],
130+
[
131+
'Unary operation "-" on mixed results in an error.',
132+
26,
133+
],
134+
[
135+
'Unary operation "~" on mixed results in an error.',
136+
27,
137+
],
138+
]);
139+
}
140+
141+
public function testUnion(): void
142+
{
143+
$this->analyse([__DIR__ . '/data/unary-union.php'], [
144+
[
145+
'Unary operation "+" on array|bool|float|int|object|string|null results in an error.',
146+
21,
147+
],
148+
[
149+
'Unary operation "-" on array|bool|float|int|object|string|null results in an error.',
150+
22,
151+
],
152+
[
153+
'Unary operation "~" on array|bool|float|int|object|string|null results in an error.',
154+
23,
155+
],
156+
[
157+
'Unary operation "+" on (array|object) results in an error.',
158+
25,
159+
],
160+
[
161+
'Unary operation "-" on (array|object) results in an error.',
162+
26,
163+
],
164+
[
165+
'Unary operation "~" on (array|object) results in an error.',
166+
27,
167+
],
42168
]);
43169
}
44170

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace InvalidUnaryMixed;
4+
5+
/**
6+
* @template T
7+
* @param T $a
8+
*/
9+
function genericMixed(mixed $a): void
10+
{
11+
var_dump(+$a);
12+
var_dump(-$a);
13+
var_dump(~$a);
14+
}
15+
16+
function explicitMixed(mixed $a): void
17+
{
18+
var_dump(+$a);
19+
var_dump(-$a);
20+
var_dump(~$a);
21+
}
22+
23+
function implicitMixed($a): void
24+
{
25+
var_dump(+$a);
26+
var_dump(-$a);
27+
var_dump(~$a);
28+
}

tests/PHPStan/Rules/Operators/data/invalid-unary.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?php
2-
2+
namespace InvalidUnary;
33
function (
44
int $i,
55
string $str
@@ -24,3 +24,39 @@ function (
2424
~$array;
2525
~1.1;
2626
};
27+
28+
/**
29+
* @param resource $r
30+
* @param numeric-string $ns
31+
*/
32+
function foo(bool $b, array $a, object $o, float $f, $r, string $ns): void
33+
{
34+
+$b;
35+
-$b;
36+
~$b;
37+
38+
+$a;
39+
-$a;
40+
~$a;
41+
42+
+$o;
43+
-$o;
44+
~$o;
45+
46+
+$f;
47+
-$f;
48+
~$f;
49+
50+
+$r;
51+
-$r;
52+
~$r;
53+
54+
+$ns;
55+
-$ns;
56+
~$ns;
57+
58+
$null = null;
59+
+$null;
60+
-$null;
61+
~$null;
62+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace UnaryBenevolentUnion;
4+
5+
/**
6+
* @param __benevolent<scalar|null|array|object> $benevolentUnion
7+
* @param numeric-string|int|float $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+
+$benevolentUnion;
14+
-$benevolentUnion;
15+
~$benevolentUnion;
16+
17+
+$okUnion;
18+
-$okUnion;
19+
~$okUnion;
20+
21+
+$union;
22+
-$union;
23+
~$union;
24+
25+
+$badBenevolentUnion;
26+
-$badBenevolentUnion;
27+
~$badBenevolentUnion;
28+
}

0 commit comments

Comments
 (0)