-
Notifications
You must be signed in to change notification settings - Fork 478
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
769669d
commit 9b7b1e1
Showing
5 changed files
with
576 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Variables; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\Expr; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Rules\Properties\PropertyDescriptor; | ||
use PHPStan\Rules\Properties\PropertyReflectionFinder; | ||
use PHPStan\Rules\RuleError; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
use PHPStan\Type\NullType; | ||
use PHPStan\Type\Type; | ||
use PHPStan\Type\VerbosityLevel; | ||
|
||
/** | ||
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> | ||
*/ | ||
class CoalesceRule implements \PHPStan\Rules\Rule | ||
{ | ||
|
||
/** @var \PHPStan\Rules\Properties\PropertyDescriptor */ | ||
private $propertyDescriptor; | ||
|
||
/** @var \PHPStan\Rules\Properties\PropertyReflectionFinder */ | ||
private $propertyReflectionFinder; | ||
|
||
public function __construct( | ||
PropertyDescriptor $propertyDescriptor, | ||
PropertyReflectionFinder $propertyReflectionFinder | ||
) | ||
{ | ||
$this->propertyDescriptor = $propertyDescriptor; | ||
$this->propertyReflectionFinder = $propertyReflectionFinder; | ||
} | ||
|
||
public function getNodeType(): string | ||
{ | ||
return \PhpParser\Node\Expr::class; | ||
} | ||
|
||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if ($node instanceof Node\Expr\BinaryOp\Coalesce) { | ||
$error = $this->canBeCoalesced($node->left, $scope, 'Coalesce'); | ||
} elseif ($node instanceof Node\Expr\AssignOp\Coalesce) { | ||
$error = $this->canBeCoalesced($node->var, $scope, 'Null-coalescing assignment'); | ||
} else { | ||
return []; | ||
} | ||
|
||
if ($error === null) { | ||
return []; | ||
} | ||
|
||
return [$error]; | ||
} | ||
|
||
private function canBeCoalesced(Node $node, Scope $scope, string $action, ?RuleError $error = null): ?RuleError | ||
{ | ||
if ($node instanceof \PhpParser\Node\Expr\FuncCall || | ||
$node instanceof \PhpParser\Node\Expr\MethodCall || | ||
$node instanceof \PhpParser\Node\Expr\StaticCall) { | ||
|
||
if ($node->name instanceof Expr) { | ||
return null; | ||
} | ||
|
||
$resultingType = $scope->getType($node); | ||
|
||
$error = $this->generateError( | ||
$resultingType, | ||
$node, | ||
sprintf('%s of return value for call to \'%s\'', $action, $node->name->toString()) | ||
); | ||
|
||
} elseif ($node instanceof Node\Expr\Variable && is_string($node->name)) { | ||
|
||
$hasVariable = $scope->hasVariableType($node->name); | ||
|
||
if ($hasVariable->no()) { | ||
return $error ?? RuleErrorBuilder::message( | ||
sprintf('%s of undefined variable $%s.', $action, $node->name) | ||
)->line($node->getLine())->build(); | ||
} | ||
|
||
$variableType = $scope->getVariableType($node->name); | ||
|
||
if ($hasVariable->maybe()) { | ||
return null; | ||
} | ||
|
||
if ($hasVariable->yes()) { | ||
return $error ?? $this->generateError( | ||
$variableType, | ||
$node, | ||
sprintf('%s of variable $%s', $action, $node->name) | ||
); | ||
} | ||
|
||
} elseif ($node instanceof Node\Expr\ArrayDimFetch && $node->dim !== null) { | ||
|
||
$type = $scope->getType($node->var); | ||
$dimType = $scope->getType($node->dim); | ||
$hasOffsetValue = $type->hasOffsetValueType($dimType); | ||
|
||
if ($hasOffsetValue->no() || $type->isOffsetAccessible()->no()) { | ||
return $error ?? RuleErrorBuilder::message( | ||
sprintf( | ||
'%s of invalid offset %s on %s.', | ||
$action, | ||
$dimType->describe(VerbosityLevel::value()), | ||
$type->describe(VerbosityLevel::value()) | ||
) | ||
)->line($node->getLine())->build(); | ||
} | ||
|
||
if ($hasOffsetValue->maybe()) { | ||
return null; | ||
} | ||
|
||
// If offset is cannot be null, store this error message and see if one of the earlier offsets is. | ||
// E.g. $array['a']['b']['c'] ?? null; is a valid coalesce if a OR b or C might be null. | ||
if ($hasOffsetValue->yes()) { | ||
|
||
$error = $error ?? $this->generateError($type->getOffsetValueType($dimType), $node, sprintf( | ||
'%s of offset %s on %s', | ||
$action, | ||
$dimType->describe(VerbosityLevel::value()), | ||
$type->describe(VerbosityLevel::value()) | ||
)); | ||
|
||
if ($error !== null) { | ||
return $this->canBeCoalesced($node->var, $scope, $action, $error); | ||
} | ||
} | ||
|
||
// Has offset, it is nullable | ||
return null; | ||
|
||
} elseif ($node instanceof Node\Expr\PropertyFetch || $node instanceof Node\Expr\StaticPropertyFetch) { | ||
|
||
$propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node, $scope); | ||
|
||
if ($propertyReflection === null) { | ||
return null; | ||
} | ||
|
||
$propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $node); | ||
$propertyType = $propertyReflection->getWritableType(); | ||
|
||
$error = $error ?? $this->generateError( | ||
$propertyReflection->getWritableType(), | ||
$node, | ||
sprintf('%s of %s (%s)', $action, $propertyDescription, $propertyType->describe(VerbosityLevel::typeOnly())) | ||
); | ||
|
||
if ($error !== null && property_exists($node, 'var')) { | ||
return $this->canBeCoalesced($node->var, $scope, $action, $error); | ||
} | ||
|
||
} | ||
|
||
return $error; | ||
} | ||
|
||
private function generateError(Type $type, Node $node, string $message): ?RuleError | ||
{ | ||
$nullType = new NullType(); | ||
|
||
if ($type->equals($nullType)) { | ||
return $this->generateAlwaysNullError($node, $message); | ||
} | ||
|
||
if ($type->isSuperTypeOf($nullType)->no()) { | ||
return $this->generateNeverNullError($node, $message); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
private function generateAlwaysNullError(Node $node, string $message): RuleError | ||
{ | ||
return RuleErrorBuilder::message( | ||
sprintf('%s, which is always null.', $message) | ||
)->line($node->getLine())->build(); | ||
} | ||
|
||
private function generateNeverNullError(Node $node, string $message): RuleError | ||
{ | ||
return RuleErrorBuilder::message( | ||
sprintf('%s, which cannot be null.', $message) | ||
)->line($node->getLine())->build(); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Variables; | ||
|
||
use PHPStan\Rules\Properties\PropertyDescriptor; | ||
use PHPStan\Rules\Properties\PropertyReflectionFinder; | ||
|
||
/** | ||
* @extends \PHPStan\Testing\RuleTestCase<CoalesceRule> | ||
*/ | ||
class CoalesceRuleTest extends \PHPStan\Testing\RuleTestCase | ||
{ | ||
|
||
protected function getRule(): \PHPStan\Rules\Rule | ||
{ | ||
return new CoalesceRule(new PropertyDescriptor(), new PropertyReflectionFinder()); | ||
} | ||
|
||
public function testCoalesceRule(): void | ||
{ | ||
require_once __DIR__ . '/data/coalesce.php'; | ||
$this->analyse([__DIR__ . '/data/coalesce.php'], [ | ||
[ | ||
'Coalesce of Property CoalesceRule\FooCoalesce::$string (string), which cannot be null.', | ||
32, | ||
], | ||
[ | ||
'Coalesce of variable $scalar, which cannot be null.', | ||
41, | ||
], | ||
[ | ||
'Coalesce of invalid offset \'string\' on array(1, 2, 3).', | ||
45, | ||
], | ||
[ | ||
'Coalesce of invalid offset \'string\' on array(array(1), array(2), array(3)).', | ||
49, | ||
], | ||
[ | ||
'Coalesce of undefined variable $doesNotExist.', | ||
51, | ||
], | ||
[ | ||
'Coalesce of offset \'dim\' on array(\'dim\' => 1, \'dim-null\' => 1|null, \'dim-null-offset\' => array(\'a\' => true|null), \'dim-empty\' => array()), which cannot be null.', | ||
67, | ||
], | ||
[ | ||
'Coalesce of invalid offset \'b\' on array().', | ||
79, | ||
], | ||
[ | ||
'Coalesce of return value for call to \'rand\', which cannot be null.', | ||
81, | ||
], | ||
[ | ||
'Coalesce of Property CoalesceRule\FooCoalesce::$string (string), which cannot be null.', | ||
89, | ||
], | ||
[ | ||
'Coalesce of Property CoalesceRule\FooCoalesce::$alwaysNull (null), which is always null.', | ||
91, | ||
], | ||
[ | ||
'Coalesce of Property CoalesceRule\FooCoalesce::$string (string), which cannot be null.', | ||
93, | ||
], | ||
[ | ||
'Coalesce of Static property CoalesceRule\FooCoalesce::$staticString (string), which cannot be null.', | ||
99, | ||
], | ||
[ | ||
'Coalesce of Static property CoalesceRule\FooCoalesce::$staticAlwaysNull (null), which is always null.', | ||
101, | ||
], | ||
[ | ||
'Coalesce of variable $a, which is always null.', | ||
115, | ||
], | ||
]); | ||
} | ||
|
||
public function testCoalesceAssignRule(): void | ||
{ | ||
if (PHP_VERSION_ID < 70400) { | ||
$this->markTestSkipped('Test requires PHP 7.4.'); | ||
} | ||
|
||
require_once __DIR__ . '/data/coalesce-assign.php'; | ||
$this->analyse([__DIR__ . '/data/coalesce-assign.php'], [ | ||
[ | ||
'Null-coalescing assignment of Property CoalesceAssignRule\FooCoalesce::$string (string), which cannot be null.', | ||
32, | ||
], | ||
[ | ||
'Null-coalescing assignment of variable $scalar, which cannot be null.', | ||
41, | ||
], | ||
[ | ||
'Null-coalescing assignment of invalid offset \'string\' on array(1, 2, 3).', | ||
45, | ||
], | ||
[ | ||
'Null-coalescing assignment of invalid offset \'string\' on array(array(1), array(2), array(3)).', | ||
49, | ||
], | ||
[ | ||
'Null-coalescing assignment of undefined variable $doesNotExist.', | ||
51, | ||
], | ||
[ | ||
'Null-coalescing assignment of offset \'dim\' on array(\'dim\' => 1, \'dim-null\' => 1|null, \'dim-null-offset\' => array(\'a\' => true|null), \'dim-empty\' => array()), which cannot be null.', | ||
67, | ||
], | ||
[ | ||
'Null-coalescing assignment of invalid offset \'b\' on array().', | ||
79, | ||
], | ||
[ | ||
'Null-coalescing assignment of Property CoalesceAssignRule\FooCoalesce::$string (string), which cannot be null.', | ||
89, | ||
], | ||
[ | ||
'Null-coalescing assignment of Property CoalesceAssignRule\FooCoalesce::$alwaysNull (null), which is always null.', | ||
91, | ||
], | ||
[ | ||
'Null-coalescing assignment of Property CoalesceAssignRule\FooCoalesce::$string (string), which cannot be null.', | ||
93, | ||
], | ||
[ | ||
'Null-coalescing assignment of Static property CoalesceAssignRule\FooCoalesce::$staticString (string), which cannot be null.', | ||
99, | ||
], | ||
[ | ||
'Null-coalescing assignment of Static property CoalesceAssignRule\FooCoalesce::$staticAlwaysNull (null), which is always null.', | ||
101, | ||
], | ||
[ | ||
'Null-coalescing assignment of variable $a, which is always null.', | ||
115, | ||
], | ||
]); | ||
} | ||
|
||
} |
Oops, something went wrong.