From 8a1f098fe5cfa2568a494e342a711e16fbf7083f Mon Sep 17 00:00:00 2001 From: schlndh Date: Fri, 19 Jul 2024 13:09:28 +0200 Subject: [PATCH] Bleeding edge - check mixed in unary operator --- conf/config.level2.neon | 7 +- .../Operators/InvalidUnaryOperationRule.php | 75 +++++++--- .../InvalidUnaryOperationRuleTest.php | 128 +++++++++++++++++- .../Operators/data/invalid-unary-mixed.php | 28 ++++ .../Rules/Operators/data/invalid-unary.php | 38 +++++- .../Rules/Operators/data/unary-union.php | 28 ++++ 6 files changed, 284 insertions(+), 20 deletions(-) create mode 100644 tests/PHPStan/Rules/Operators/data/invalid-unary-mixed.php create mode 100644 tests/PHPStan/Rules/Operators/data/unary-union.php diff --git a/conf/config.level2.neon b/conf/config.level2.neon index ec0c93f33f..907c83e394 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -30,7 +30,6 @@ rules: - PHPStan\Rules\Generics\UsedTraitsRule - PHPStan\Rules\Methods\CallPrivateMethodThroughStaticRule - PHPStan\Rules\Methods\IncompatibleDefaultParameterTypeRule - - PHPStan\Rules\Operators\InvalidUnaryOperationRule - PHPStan\Rules\Operators\InvalidComparisonOperationRule - PHPStan\Rules\PhpDoc\FunctionConditionalReturnTypeRule - PHPStan\Rules\PhpDoc\MethodConditionalReturnTypeRule @@ -143,3 +142,9 @@ services: bleedingEdge: %featureToggles.bleedingEdge% tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Operators\InvalidUnaryOperationRule + arguments: + bleedingEdge: %featureToggles.bleedingEdge% + tags: + - phpstan.rules.rule diff --git a/src/Rules/Operators/InvalidUnaryOperationRule.php b/src/Rules/Operators/InvalidUnaryOperationRule.php index 000b0529f2..099c1d6507 100644 --- a/src/Rules/Operators/InvalidUnaryOperationRule.php +++ b/src/Rules/Operators/InvalidUnaryOperationRule.php @@ -3,10 +3,14 @@ namespace PHPStan\Rules\Operators; use PhpParser\Node; +use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Rules\RuleLevelHelper; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ErrorType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -16,6 +20,13 @@ class InvalidUnaryOperationRule implements Rule { + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + private bool $bleedingEdge, + ) + { + } + public function getNodeType(): string { return Node\Expr::class; @@ -31,28 +42,58 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($scope->getType($node) instanceof ErrorType) { + if ($this->bleedingEdge) { + $varName = '__PHPSTAN__LEFT__'; + $variable = new Node\Expr\Variable($varName); + $newNode = clone $node; + $newNode->setAttribute('phpstan_cache_printer', null); + $newNode->expr = $variable; - if ($node instanceof Node\Expr\UnaryPlus) { - $operator = '+'; - } elseif ($node instanceof Node\Expr\UnaryMinus) { - $operator = '-'; + if ($node instanceof Node\Expr\BitwiseNot) { + $callback = static fn (Type $type): bool => $type->isString()->yes() || $type->isInteger()->yes() || $type->isFloat()->yes(); } else { - $operator = '~'; + $callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType; } - return [ - RuleErrorBuilder::message(sprintf( - 'Unary operation "%s" on %s results in an error.', - $operator, - $scope->getType($node->expr)->describe(VerbosityLevel::value()), - )) - ->line($node->expr->getStartLine()) - ->identifier('unaryOp.invalid') - ->build(), - ]; + + $exprType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->expr, + '', + $callback, + )->getType(); + if ($exprType instanceof ErrorType) { + return []; + } + + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $scope = $scope->assignVariable($varName, $exprType, $exprType); + if (!$scope->getType($newNode) instanceof ErrorType) { + return []; + } + } elseif (!$scope->getType($node) instanceof ErrorType) { + return []; } - return []; + if ($node instanceof Node\Expr\UnaryPlus) { + $operator = '+'; + } elseif ($node instanceof Node\Expr\UnaryMinus) { + $operator = '-'; + } else { + $operator = '~'; + } + return [ + RuleErrorBuilder::message(sprintf( + 'Unary operation "%s" on %s results in an error.', + $operator, + $scope->getType($node->expr)->describe(VerbosityLevel::value()), + )) + ->line($node->expr->getStartLine()) + ->identifier('unaryOp.invalid') + ->build(), + ]; } } diff --git a/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php index fbc6a265f9..2475fa3a80 100644 --- a/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Operators; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; /** @@ -11,9 +12,16 @@ class InvalidUnaryOperationRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { - return new InvalidUnaryOperationRule(); + return new InvalidUnaryOperationRule( + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false), + true, + ); } public function testRule(): void @@ -39,6 +47,124 @@ public function testRule(): void 'Unary operation "~" on array{} results in an error.', 24, ], + [ + 'Unary operation "~" on bool results in an error.', + 36, + ], + [ + 'Unary operation "+" on array results in an error.', + 38, + ], + [ + 'Unary operation "-" on array results in an error.', + 39, + ], + [ + 'Unary operation "~" on array results in an error.', + 40, + ], + [ + 'Unary operation "+" on object results in an error.', + 42, + ], + [ + 'Unary operation "-" on object results in an error.', + 43, + ], + [ + 'Unary operation "~" on object results in an error.', + 44, + ], + [ + 'Unary operation "+" on resource results in an error.', + 50, + ], + [ + 'Unary operation "-" on resource results in an error.', + 51, + ], + [ + 'Unary operation "~" on resource results in an error.', + 52, + ], + [ + 'Unary operation "~" on null results in an error.', + 61, + ], + ]); + } + + public function testMixed(): void + { + $this->checkImplicitMixed = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/invalid-unary-mixed.php'], [ + [ + 'Unary operation "+" on T results in an error.', + 11, + ], + [ + 'Unary operation "-" on T results in an error.', + 12, + ], + [ + 'Unary operation "~" on T results in an error.', + 13, + ], + [ + 'Unary operation "+" on mixed results in an error.', + 18, + ], + [ + 'Unary operation "-" on mixed results in an error.', + 19, + ], + [ + 'Unary operation "~" on mixed results in an error.', + 20, + ], + [ + 'Unary operation "+" on mixed results in an error.', + 25, + ], + [ + 'Unary operation "-" on mixed results in an error.', + 26, + ], + [ + 'Unary operation "~" on mixed results in an error.', + 27, + ], + ]); + } + + public function testUnion(): void + { + $this->analyse([__DIR__ . '/data/unary-union.php'], [ + [ + 'Unary operation "+" on array|bool|float|int|object|string|null results in an error.', + 21, + ], + [ + 'Unary operation "-" on array|bool|float|int|object|string|null results in an error.', + 22, + ], + [ + 'Unary operation "~" on array|bool|float|int|object|string|null results in an error.', + 23, + ], + [ + 'Unary operation "+" on (array|object) results in an error.', + 25, + ], + [ + 'Unary operation "-" on (array|object) results in an error.', + 26, + ], + [ + 'Unary operation "~" on (array|object) results in an error.', + 27, + ], ]); } diff --git a/tests/PHPStan/Rules/Operators/data/invalid-unary-mixed.php b/tests/PHPStan/Rules/Operators/data/invalid-unary-mixed.php new file mode 100644 index 0000000000..a82faa213a --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/invalid-unary-mixed.php @@ -0,0 +1,28 @@ + $benevolentUnion + * @param numeric-string|int|float $okUnion + * @param scalar|null|array|object $union + * @param __benevolent $badBenevolentUnion + */ +function foo($benevolentUnion, $okUnion, $union, $badBenevolentUnion): void +{ + +$benevolentUnion; + -$benevolentUnion; + ~$benevolentUnion; + + +$okUnion; + -$okUnion; + ~$okUnion; + + +$union; + -$union; + ~$union; + + +$badBenevolentUnion; + -$badBenevolentUnion; + ~$badBenevolentUnion; +}