mirror of
https://github.com/rectorphp/rector.git
synced 2024-06-12 22:22:24 +00:00
7e7a2f067a
476cfb00cb
disable fallback for now
176 lines
6.1 KiB
PHP
176 lines
6.1 KiB
PHP
<?php
|
|
|
|
declare (strict_types=1);
|
|
namespace Rector\CodeQuality\Rector\Catch_;
|
|
|
|
use PhpParser\Node;
|
|
use PhpParser\Node\Arg;
|
|
use PhpParser\Node\Expr\MethodCall;
|
|
use PhpParser\Node\Expr\New_;
|
|
use PhpParser\Node\Expr\Variable;
|
|
use PhpParser\Node\Identifier;
|
|
use PhpParser\Node\Name;
|
|
use PhpParser\Node\Stmt\Catch_;
|
|
use PhpParser\Node\Stmt\Throw_;
|
|
use PhpParser\NodeTraverser;
|
|
use PHPStan\Reflection\ParametersAcceptorSelector;
|
|
use PHPStan\Reflection\ReflectionProvider;
|
|
use PHPStan\Type\ObjectType;
|
|
use PHPStan\Type\TypeWithClassName;
|
|
use Rector\NodeTypeResolver\Node\AttributeKey;
|
|
use Rector\Rector\AbstractRector;
|
|
use Rector\ValueObject\MethodName;
|
|
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
|
|
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
|
|
/**
|
|
* @changelog https://github.com/thecodingmachine/phpstan-strict-rules/blob/e3d746a61d38993ca2bc2e2fcda7012150de120c/src/Rules/Exceptions/ThrowMustBundlePreviousExceptionRule.php#L83
|
|
* @see \Rector\Tests\CodeQuality\Rector\Catch_\ThrowWithPreviousExceptionRector\ThrowWithPreviousExceptionRectorTest
|
|
*/
|
|
final class ThrowWithPreviousExceptionRector extends AbstractRector
|
|
{
|
|
/**
|
|
* @readonly
|
|
* @var \PHPStan\Reflection\ReflectionProvider
|
|
*/
|
|
private $reflectionProvider;
|
|
/**
|
|
* @var int
|
|
*/
|
|
private const DEFAULT_EXCEPTION_ARGUMENT_POSITION = 2;
|
|
public function __construct(ReflectionProvider $reflectionProvider)
|
|
{
|
|
$this->reflectionProvider = $reflectionProvider;
|
|
}
|
|
public function getRuleDefinition() : RuleDefinition
|
|
{
|
|
return new RuleDefinition('When throwing into a catch block, checks that the previous exception is passed to the new throw clause', [new CodeSample(<<<'CODE_SAMPLE'
|
|
class SomeClass
|
|
{
|
|
public function run()
|
|
{
|
|
try {
|
|
$someCode = 1;
|
|
} catch (Throwable $throwable) {
|
|
throw new AnotherException('ups');
|
|
}
|
|
}
|
|
}
|
|
CODE_SAMPLE
|
|
, <<<'CODE_SAMPLE'
|
|
class SomeClass
|
|
{
|
|
public function run()
|
|
{
|
|
try {
|
|
$someCode = 1;
|
|
} catch (Throwable $throwable) {
|
|
throw new AnotherException('ups', $throwable->getCode(), $throwable);
|
|
}
|
|
}
|
|
}
|
|
CODE_SAMPLE
|
|
)]);
|
|
}
|
|
/**
|
|
* @return array<class-string<Node>>
|
|
*/
|
|
public function getNodeTypes() : array
|
|
{
|
|
return [Catch_::class];
|
|
}
|
|
/**
|
|
* @param Catch_ $node
|
|
*/
|
|
public function refactor(Node $node) : ?Node
|
|
{
|
|
$caughtThrowableVariable = $node->var;
|
|
if (!$caughtThrowableVariable instanceof Variable) {
|
|
return null;
|
|
}
|
|
$isChanged = \false;
|
|
$this->traverseNodesWithCallable($node->stmts, function (Node $node) use($caughtThrowableVariable, &$isChanged) : ?int {
|
|
if (!$node instanceof Throw_) {
|
|
return null;
|
|
}
|
|
$isChanged = $this->refactorThrow($node, $caughtThrowableVariable);
|
|
return $isChanged;
|
|
});
|
|
if (!(bool) $isChanged) {
|
|
return null;
|
|
}
|
|
return $node;
|
|
}
|
|
private function refactorThrow(Throw_ $throw, Variable $catchedThrowableVariable) : ?int
|
|
{
|
|
if (!$throw->expr instanceof New_) {
|
|
return null;
|
|
}
|
|
$new = $throw->expr;
|
|
if (!$new->class instanceof Name) {
|
|
return null;
|
|
}
|
|
$exceptionArgumentPosition = $this->resolveExceptionArgumentPosition($new->class);
|
|
if ($exceptionArgumentPosition === null) {
|
|
return null;
|
|
}
|
|
if ($new->isFirstClassCallable()) {
|
|
return null;
|
|
}
|
|
// exception is bundled
|
|
if (isset($new->getArgs()[$exceptionArgumentPosition])) {
|
|
return null;
|
|
}
|
|
if (!isset($new->getArgs()[0])) {
|
|
// get previous message
|
|
$getMessageMethodCall = new MethodCall($catchedThrowableVariable, 'getMessage');
|
|
$new->args[0] = new Arg($getMessageMethodCall);
|
|
}
|
|
if (!isset($new->getArgs()[1])) {
|
|
// get previous code
|
|
$new->args[1] = new Arg(new MethodCall($catchedThrowableVariable, 'getCode'));
|
|
}
|
|
/** @var Arg $arg1 */
|
|
$arg1 = $new->args[1];
|
|
if ($arg1->name instanceof Identifier && $arg1->name->toString() === 'previous') {
|
|
$new->args[1] = new Arg(new MethodCall($catchedThrowableVariable, 'getCode'));
|
|
$new->args[$exceptionArgumentPosition] = $arg1;
|
|
} else {
|
|
$new->args[$exceptionArgumentPosition] = new Arg($catchedThrowableVariable);
|
|
}
|
|
// null the node, to fix broken format preserving printers, see https://github.com/rectorphp/rector/issues/5576
|
|
$new->setAttribute(AttributeKey::ORIGINAL_NODE, null);
|
|
// nothing more to add
|
|
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
|
|
}
|
|
private function resolveExceptionArgumentPosition(Name $exceptionName) : ?int
|
|
{
|
|
$className = $this->getName($exceptionName);
|
|
// is native exception?
|
|
if (\strpos($className, '\\') === \false) {
|
|
return self::DEFAULT_EXCEPTION_ARGUMENT_POSITION;
|
|
}
|
|
if (!$this->reflectionProvider->hasClass($className)) {
|
|
return self::DEFAULT_EXCEPTION_ARGUMENT_POSITION;
|
|
}
|
|
$classReflection = $this->reflectionProvider->getClass($className);
|
|
$construct = $classReflection->hasMethod(MethodName::CONSTRUCT);
|
|
if (!$construct) {
|
|
return self::DEFAULT_EXCEPTION_ARGUMENT_POSITION;
|
|
}
|
|
$extendedMethodReflection = $classReflection->getConstructor();
|
|
$parametersAcceptorWithPhpDocs = ParametersAcceptorSelector::selectSingle($extendedMethodReflection->getVariants());
|
|
foreach ($parametersAcceptorWithPhpDocs->getParameters() as $position => $parameterReflectionWithPhpDoc) {
|
|
$parameterType = $parameterReflectionWithPhpDoc->getType();
|
|
if (!$parameterType instanceof TypeWithClassName) {
|
|
continue;
|
|
}
|
|
$objectType = new ObjectType('Throwable');
|
|
if ($objectType->isSuperTypeOf($parameterType)->no()) {
|
|
continue;
|
|
}
|
|
return $position;
|
|
}
|
|
return null;
|
|
}
|
|
}
|