Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
75 changes: 58 additions & 17 deletions src/Rules/Operators/InvalidUnaryOperationRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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(),
];
}

}
128 changes: 127 additions & 1 deletion tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Rules\Operators;

use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Testing\RuleTestCase;

/**
Expand All @@ -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
Expand All @@ -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,
],
]);
}

Expand Down
28 changes: 28 additions & 0 deletions tests/PHPStan/Rules/Operators/data/invalid-unary-mixed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);

namespace InvalidUnaryMixed;

/**
* @template T
* @param T $a
*/
function genericMixed(mixed $a): void
{
var_dump(+$a);
var_dump(-$a);
var_dump(~$a);
}

function explicitMixed(mixed $a): void
{
var_dump(+$a);
var_dump(-$a);
var_dump(~$a);
}

function implicitMixed($a): void
{
var_dump(+$a);
var_dump(-$a);
var_dump(~$a);
}
38 changes: 37 additions & 1 deletion tests/PHPStan/Rules/Operators/data/invalid-unary.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?php

namespace InvalidUnary;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was getting some weird unrelated test failures (probably due to adding foo to the global namespace), so I added the namespace here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes 👍

function (
int $i,
string $str
Expand All @@ -24,3 +24,39 @@ function (
~$array;
~1.1;
};

/**
* @param resource $r
* @param numeric-string $ns
*/
function foo(bool $b, array $a, object $o, float $f, $r, string $ns): void
{
+$b;
-$b;
~$b;

+$a;
-$a;
~$a;

+$o;
-$o;
~$o;

+$f;
-$f;
~$f;

+$r;
-$r;
~$r;

+$ns;
-$ns;
~$ns;

$null = null;
+$null;
-$null;
~$null;
}
28 changes: 28 additions & 0 deletions tests/PHPStan/Rules/Operators/data/unary-union.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);

namespace UnaryBenevolentUnion;

/**
* @param __benevolent<scalar|null|array|object> $benevolentUnion
* @param numeric-string|int|float $okUnion
* @param scalar|null|array|object $union
* @param __benevolent<array|object> $badBenevolentUnion
*/
function foo($benevolentUnion, $okUnion, $union, $badBenevolentUnion): void
{
+$benevolentUnion;
-$benevolentUnion;
~$benevolentUnion;

+$okUnion;
-$okUnion;
~$okUnion;

+$union;
-$union;
~$union;

+$badBenevolentUnion;
-$badBenevolentUnion;
~$badBenevolentUnion;
}