diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 4f9da39886..18bc1b0cd5 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -205,6 +205,7 @@ services: tags: - phpstan.rules.rule arguments: + bleedingEdge: %featureToggles.bleedingEdge% checkThisOnly: %checkThisOnly% - diff --git a/src/Rules/Operators/InvalidIncDecOperationRule.php b/src/Rules/Operators/InvalidIncDecOperationRule.php index b3e4a29263..9cd551cbf3 100644 --- a/src/Rules/Operators/InvalidIncDecOperationRule.php +++ b/src/Rules/Operators/InvalidIncDecOperationRule.php @@ -6,8 +6,17 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\BooleanType; use PHPStan\Type\ErrorType; +use PHPStan\Type\FloatType; +use PHPStan\Type\IntegerType; +use PHPStan\Type\NullType; +use PHPStan\Type\ObjectType; +use PHPStan\Type\StringType; +use PHPStan\Type\Type; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function get_class; use function sprintf; @@ -18,7 +27,11 @@ class InvalidIncDecOperationRule implements Rule { - public function __construct(private bool $checkThisOnly) + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + private bool $bleedingEdge, + private bool $checkThisOnly, + ) { } @@ -74,7 +87,11 @@ public function processNode(Node $node, Scope $scope): array ]; } - if (!$this->checkThisOnly) { + if (!$this->bleedingEdge) { + if ($this->checkThisOnly) { + return []; + } + $varType = $scope->getType($node->var); if (!$varType->toString() instanceof ErrorType) { return []; @@ -82,20 +99,30 @@ public function processNode(Node $node, Scope $scope): array if (!$varType->toNumber() instanceof ErrorType) { return []; } + } else { + $allowedTypes = new UnionType([new BooleanType(), new FloatType(), new IntegerType(), new StringType(), new NullType(), new ObjectType('SimpleXMLElement')]); + $varType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->var, + '', + static fn (Type $type): bool => $allowedTypes->isSuperTypeOf($type)->yes(), + )->getType(); - return [ - RuleErrorBuilder::message(sprintf( - 'Cannot use %s on %s.', - $operatorString, - $varType->describe(VerbosityLevel::value()), - )) - ->line($node->var->getStartLine()) - ->identifier(sprintf('%s.type', $nodeType)) - ->build(), - ]; + if ($varType instanceof ErrorType || $allowedTypes->isSuperTypeOf($varType)->yes()) { + return []; + } } - return []; + return [ + RuleErrorBuilder::message(sprintf( + 'Cannot use %s on %s.', + $operatorString, + $varType->describe(VerbosityLevel::value()), + )) + ->line($node->var->getStartLine()) + ->identifier(sprintf('%s.type', $nodeType)) + ->build(), + ]; } } diff --git a/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php index 7eda97b50c..5042bb336c 100644 --- a/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Operators; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; /** @@ -11,9 +12,17 @@ class InvalidIncDecOperationRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { - return new InvalidIncDecOperationRule(false); + return new InvalidIncDecOperationRule( + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false), + true, + false, + ); } public function testRule(): void @@ -31,6 +40,108 @@ public function testRule(): void 'Cannot use ++ on stdClass.', 17, ], + [ + 'Cannot use ++ on InvalidIncDec\\ClassWithToString.', + 19, + ], + [ + 'Cannot use -- on InvalidIncDec\\ClassWithToString.', + 21, + ], + [ + 'Cannot use ++ on array{}.', + 23, + ], + [ + 'Cannot use -- on array{}.', + 25, + ], + [ + 'Cannot use ++ on resource.', + 28, + ], + [ + 'Cannot use -- on resource.', + 32, + ], + ]); + } + + public function testMixed(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/invalid-inc-dec-mixed.php'], [ + [ + 'Cannot use ++ on T of mixed.', + 12, + ], + [ + 'Cannot use ++ on T of mixed.', + 14, + ], + [ + 'Cannot use -- on T of mixed.', + 16, + ], + [ + 'Cannot use -- on T of mixed.', + 18, + ], + [ + 'Cannot use ++ on mixed.', + 24, + ], + [ + 'Cannot use ++ on mixed.', + 26, + ], + [ + 'Cannot use -- on mixed.', + 28, + ], + [ + 'Cannot use -- on mixed.', + 30, + ], + [ + 'Cannot use ++ on mixed.', + 36, + ], + [ + 'Cannot use ++ on mixed.', + 38, + ], + [ + 'Cannot use -- on mixed.', + 40, + ], + [ + 'Cannot use -- on mixed.', + 42, + ], + ]); + } + + public function testUnion(): void + { + $this->analyse([__DIR__ . '/data/invalid-inc-dec-union.php'], [ + [ + 'Cannot use ++ on array|bool|float|int|object|string|null.', + 24, + ], + [ + 'Cannot use -- on array|bool|float|int|object|string|null.', + 26, + ], + [ + 'Cannot use ++ on (array|object).', + 29, + ], + [ + 'Cannot use -- on (array|object).', + 31, + ], ]); } diff --git a/tests/PHPStan/Rules/Operators/data/invalid-inc-dec-mixed.php b/tests/PHPStan/Rules/Operators/data/invalid-inc-dec-mixed.php new file mode 100644 index 0000000000..4e190cc73c --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/invalid-inc-dec-mixed.php @@ -0,0 +1,43 @@ + $benevolentUnion + * @param string|int|float|bool|null $okUnion + * @param scalar|null|array|object $union + * @param __benevolent $badBenevolentUnion + */ +function foo($benevolentUnion, $okUnion, $union, $badBenevolentUnion): void +{ + $a = $benevolentUnion; + $a++; + $a = $benevolentUnion; + --$a; + + $a = $okUnion; + $a++; + $a = $okUnion; + --$a; + + $a = $union; + $a++; + $a = $union; + --$a; + + $a = $badBenevolentUnion; + $a++; + $a = $badBenevolentUnion; + --$a; +} diff --git a/tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php b/tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php index 9e551e875f..aee9fba6fa 100644 --- a/tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php +++ b/tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php @@ -2,7 +2,7 @@ namespace InvalidIncDec; -function ($a, int $i, ?float $j, string $str, \stdClass $std) { +function ($a, int $i, ?float $j, string $str, \stdClass $std, \SimpleXMLElement $simpleXMLElement) { $a++; $b = [1]; @@ -15,4 +15,41 @@ function ($a, int $i, ?float $j, string $str, \stdClass $std) { $j++; $str++; $std++; + $classWithToString = new ClassWithToString(); + $classWithToString++; + $classWithToString = new ClassWithToString(); + --$classWithToString; + $arr = []; + $arr++; + $arr = []; + --$arr; + + if (($f = fopen('php://stdin', 'r')) !== false) { + $f++; + } + + if (($f = fopen('php://stdin', 'r')) !== false) { + --$f; + } + + $bool = true; + $bool++; + $bool = false; + --$bool; + $null = null; + $null++; + $null = null; + --$null; + $a = $simpleXMLElement; + $a++; + $a = $simpleXMLElement; + --$a; }; + +class ClassWithToString +{ + public function __toString(): string + { + return 'foo'; + } +}