Skip to content

Commit 79172a7

Browse files
authored
forbidIncrementDecrementOnNonInteger (#179)
1 parent 588ead5 commit 79172a7

File tree

5 files changed

+153
-0
lines changed

5 files changed

+153
-0
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ parameters:
8080
forbidIdenticalClassComparison:
8181
enabled: true
8282
blacklist: ['DateTimeInterface']
83+
forbidIncrementDecrementOnNonInteger:
84+
enabled: true
8385
forbidMatchDefaultArmForEnums:
8486
enabled: true
8587
forbidMethodCallOnMixed:
@@ -516,6 +518,15 @@ parameters:
516518
- Ramsey\Uuid\UuidInterface
517519
```
518520

521+
### forbidIncrementDecrementOnNonInteger
522+
- Denies using `$i++`, `$i--`, `++$i`, `--$i` with any non-integer
523+
- PHP itself is leading towards stricter behaviour here and soft-deprecated **some** non-integer usages in 8.3, see [RFC](https://wiki.php.net/rfc/saner-inc-dec-operators)
524+
525+
```php
526+
$value = '2e0';
527+
$value++; // would be float(3), denied
528+
```
529+
519530
### forbidMatchDefaultArmForEnums
520531
- Denies using default arm in `match()` construct when native enum is passed as subject
521532
- This rules makes sense only as a complement of [native phpstan rule](https://github.com/phpstan/phpstan-src/blob/1.7.x/src/Rules/Comparison/MatchExpressionRule.php#L94) that guards that all enum cases are handled in match arms

rules.neon

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ parameters:
5959
forbidIdenticalClassComparison:
6060
enabled: true
6161
blacklist: ['DateTimeInterface']
62+
forbidIncrementDecrementOnNonInteger:
63+
enabled: true
6264
forbidMatchDefaultArmForEnums:
6365
enabled: true
6466
forbidMethodCallOnMixed:
@@ -156,6 +158,9 @@ parametersSchema:
156158
enabled: bool()
157159
blacklist: arrayOf(string())
158160
])
161+
forbidIncrementDecrementOnNonInteger: structure([
162+
enabled: bool()
163+
])
159164
forbidMatchDefaultArmForEnums: structure([
160165
enabled: bool()
161166
])
@@ -245,6 +250,8 @@ conditionalTags:
245250
phpstan.rules.rule: %shipmonkRules.forbidFetchOnMixed.enabled%
246251
ShipMonk\PHPStan\Rule\ForbidIdenticalClassComparisonRule:
247252
phpstan.rules.rule: %shipmonkRules.forbidIdenticalClassComparison.enabled%
253+
ShipMonk\PHPStan\Rule\ForbidIncrementDecrementOnNonIntegerRule:
254+
phpstan.rules.rule: %shipmonkRules.forbidIncrementDecrementOnNonInteger.enabled%
248255
ShipMonk\PHPStan\Rule\ForbidMatchDefaultArmForEnumsRule:
249256
phpstan.rules.rule: %shipmonkRules.forbidMatchDefaultArmForEnums.enabled%
250257
ShipMonk\PHPStan\Rule\ForbidMethodCallOnMixedRule:
@@ -348,6 +355,8 @@ services:
348355
class: ShipMonk\PHPStan\Rule\ForbidIdenticalClassComparisonRule
349356
arguments:
350357
blacklist: %shipmonkRules.forbidIdenticalClassComparison.blacklist%
358+
-
359+
class: ShipMonk\PHPStan\Rule\ForbidIncrementDecrementOnNonIntegerRule
351360
-
352361
class: ShipMonk\PHPStan\Rule\ForbidMethodCallOnMixedRule
353362
arguments:
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\Rule;
4+
5+
use LogicException;
6+
use PhpParser\Node;
7+
use PhpParser\Node\Expr\PostDec;
8+
use PhpParser\Node\Expr\PostInc;
9+
use PhpParser\Node\Expr\PreDec;
10+
use PhpParser\Node\Expr\PreInc;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleError;
14+
use PHPStan\Rules\RuleErrorBuilder;
15+
use PHPStan\Type\VerbosityLevel;
16+
use function get_class;
17+
use function sprintf;
18+
19+
/**
20+
* @implements Rule<Node>
21+
*/
22+
class ForbidIncrementDecrementOnNonIntegerRule implements Rule
23+
{
24+
25+
public function getNodeType(): string
26+
{
27+
return Node::class;
28+
}
29+
30+
/**
31+
* @return list<RuleError>
32+
*/
33+
public function processNode(Node $node, Scope $scope): array
34+
{
35+
if (
36+
$node instanceof PostInc
37+
|| $node instanceof PostDec
38+
|| $node instanceof PreInc
39+
|| $node instanceof PreDec
40+
) {
41+
return $this->process($node, $scope);
42+
}
43+
44+
return [];
45+
}
46+
47+
/**
48+
* @param PostInc|PostDec|PreInc|PreDec $node
49+
* @return list<RuleError>
50+
*/
51+
private function process(Node $node, Scope $scope): array
52+
{
53+
$exprType = $scope->getType($node->var);
54+
55+
if (!$exprType->isInteger()->yes()) {
56+
$errorMessage = sprintf(
57+
'Using %s over non-integer (%s)',
58+
$this->getIncDecSymbol($node),
59+
$exprType->describe(VerbosityLevel::typeOnly()),
60+
);
61+
$error = RuleErrorBuilder::message($errorMessage)
62+
->identifier('shipmonk.incrementDecrementOnNonInteger')
63+
->build();
64+
return [$error];
65+
}
66+
67+
return [];
68+
}
69+
70+
/**
71+
* @param PostInc|PostDec|PreInc|PreDec $node
72+
*/
73+
private function getIncDecSymbol(Node $node): string
74+
{
75+
switch (get_class($node)) {
76+
case PostInc::class:
77+
case PreInc::class:
78+
return '++';
79+
80+
case PostDec::class:
81+
case PreDec::class:
82+
return '--';
83+
84+
default:
85+
throw new LogicException('Unexpected node given: ' . get_class($node));
86+
}
87+
}
88+
89+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\Rule;
4+
5+
use PHPStan\Rules\Rule;
6+
use ShipMonk\PHPStan\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<ForbidIncrementDecrementOnNonIntegerRule>
10+
*/
11+
class ForbidIncrementDecrementOnNonIntegerRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new ForbidIncrementDecrementOnNonIntegerRule();
17+
}
18+
19+
public function testClass(): void
20+
{
21+
$this->analyseFile(__DIR__ . '/data/ForbidIncrementDecrementOnNonIntegerRule/code.php');
22+
}
23+
24+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ForbidIncrementDecrementOnNonIntegerRule;
4+
5+
6+
class IncDec
7+
{
8+
9+
public function test(int $int, string $string, float $float, int|string $union, array $array, $mixed)
10+
{
11+
$int++;
12+
$string++; // error: Using ++ over non-integer (string)
13+
$float++; // error: Using ++ over non-integer (float)
14+
$union++; // error: Using ++ over non-integer (int|string)
15+
$array++; // error: Using ++ over non-integer (array)
16+
$mixed++; // error: Using ++ over non-integer (mixed)
17+
}
18+
}
19+
20+

0 commit comments

Comments
 (0)