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
18 changes: 18 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,24 @@ parameters:
count: 1
path: src/Analyser/StmtHandler/ClassLikeHandler.php

-
rawMessage: '''
Call to method __construct() of deprecated class PHPStan\Node\CatchWithUnthrownExceptionNode:
Use CatchTypeNode, emitted for every catch type, and check isThrown().
'''
identifier: method.deprecatedClass
count: 1
path: src/Analyser/StmtHandler/TryCatchHandler.php

-
rawMessage: '''
Instantiation of deprecated class PHPStan\Node\CatchWithUnthrownExceptionNode:
Use CatchTypeNode, emitted for every catch type, and check isThrown().
'''
identifier: new.deprecatedClass
count: 1
path: src/Analyser/StmtHandler/TryCatchHandler.php

-
rawMessage: 'Template type TNodeType is declared as covariant, but occurs in contravariant position in parameter node of method PHPStan\Collectors\Collector::processNode().'
identifier: generics.variance
Expand Down
4 changes: 4 additions & 0 deletions src/Analyser/StmtHandler/TryCatchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use PHPStan\Analyser\VariableFlowBuilder;
use PHPStan\Dependency\Dependencies;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\CatchTypeNode;
use PHPStan\Node\CatchWithUnthrownExceptionNode;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Node\FinallyExitPointsNode;
Expand Down Expand Up @@ -112,9 +113,11 @@ public function processStmt(
}
$dependencies[] = Dependencies::create($scope->getFile(), classNames: $caughtClassNames);

$catchTypeNodes = [];
$originalCatchTypes = [];
$catchTypes = [];
foreach ($catchNode->types as $catchNodeType) {
$catchTypeNodes[] = $catchNodeType;
$catchType = new ObjectType($catchNodeType->toString());
$originalCatchTypes[] = $catchType;
$catchTypes[] = TypeCombinator::remove($catchType, $pastCatchTypes);
Expand Down Expand Up @@ -207,6 +210,7 @@ public function processStmt(

// emit error
foreach ($matchingCatchTypes as $catchTypeIndex => $matched) {
$nodeScopeResolver->callNodeCallback($nodeCallback, new CatchTypeNode($catchNode, $catchTypeNodes[$catchTypeIndex], $catchTypes[$catchTypeIndex], $originalCatchTypes[$catchTypeIndex], $matched), $scope, $storage);
if ($matched) {
continue;
}
Expand Down
79 changes: 79 additions & 0 deletions src/Node/CatchTypeNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php declare(strict_types = 1);

namespace PHPStan\Node;

use Override;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Catch_;
use PhpParser\NodeAbstract;
use PHPStan\Turbo\ReferencedByTurboExtension;
use PHPStan\Type\Type;

/**
* Virtual node emitted once for each type in a `catch` clause, whether or not
* the try block can throw it - unlike CatchWithUnthrownExceptionNode, which is
* emitted only for the dead ones. Rules analysing a trait need both outcomes
* to tell whether a catch is dead in every class using it.
*
* @api
*/
#[ReferencedByTurboExtension(key: 'catchTypeNode')]
final class CatchTypeNode extends NodeAbstract implements VirtualNode
{

public function __construct(
private Catch_ $originalNode,
private Name $typeNode,
private Type $caughtType,
private Type $originalCaughtType,
private bool $thrown,
)
{
parent::__construct($originalNode->getAttributes());
}

public function getOriginalNode(): Catch_
{
return $this->originalNode;
}

public function getTypeNode(): Name
{
return $this->typeNode;
}

/**
* The caught type without the types caught by the preceding catch clauses;
* NeverType when they already catch all of it.
*/
public function getCaughtType(): Type
{
return $this->caughtType;
}

public function getOriginalCaughtType(): Type
{
return $this->originalCaughtType;
}

public function isThrown(): bool
{
return $this->thrown;
}

#[Override]
public function getType(): string
{
return 'PHPStan_Node_CatchTypeNode';
}

/**
* @return string[]
*/
#[Override]
public function getSubNodeNames(): array
{
return [];
}

}
1 change: 1 addition & 0 deletions src/Node/CatchWithUnthrownExceptionNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

/**
* @api
* @deprecated Use CatchTypeNode, emitted for every catch type, and check isThrown().
*/
#[ReferencedByTurboExtension(key: 'catchWithUnthrownExceptionNode')]
final class CatchWithUnthrownExceptionNode extends NodeAbstract implements VirtualNode
Expand Down
9 changes: 5 additions & 4 deletions src/Rules/Comparison/ConstantConditionInTraitHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PHPStan\Analyser\CollectedDataEmitter;
use PHPStan\Analyser\NodeCallbackInvoker;
use PHPStan\Analyser\RuleErrorTransformer;
Expand All @@ -27,9 +28,9 @@ public function __construct(
{
}

private function exprString(Expr $expr): string
private function exprString(Expr|Name $expr): string
{
return sprintf('%s:%d', $this->exprPrinter->printExpr($expr), $expr->getStartLine());
return sprintf('%s:%d', $expr instanceof Name ? $expr->toString() : $this->exprPrinter->printExpr($expr), $expr->getStartLine());
}

/**
Expand All @@ -38,7 +39,7 @@ private function exprString(Expr $expr): string
public function emitNoError(
string $ruleName,
Scope&NodeCallbackInvoker&CollectedDataEmitter $scope,
Expr $expr,
Expr|Name $expr,
): void
{
if (!$scope->isInTrait()) {
Expand All @@ -59,7 +60,7 @@ public function emitNoError(
public function emitError(
string $ruleName,
Scope&NodeCallbackInvoker&CollectedDataEmitter $scope,
Expr $expr,
Expr|Name $expr,
bool $value,
RuleError $ruleError,
): void
Expand Down
69 changes: 42 additions & 27 deletions src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@
namespace PHPStan\Rules\Exceptions;

use PhpParser\Node;
use PHPStan\Analyser\CollectedDataEmitter;
use PHPStan\Analyser\NodeCallbackInvoker;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Node\CatchWithUnthrownExceptionNode;
use PHPStan\Node\CatchTypeNode;
use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\NeverType;
use PHPStan\Type\VerbosityLevel;
use function sprintf;

/**
* @implements Rule<CatchWithUnthrownExceptionNode>
* @implements Rule<CatchTypeNode>
*/
#[RegisteredRule(level: 4)]
final class CatchWithUnthrownExceptionRule implements Rule
Expand All @@ -23,6 +26,7 @@ final class CatchWithUnthrownExceptionRule implements Rule
public function __construct(
#[AutowiredParameter(ref: '@exceptionTypeResolver')]
private ExceptionTypeResolver $exceptionTypeResolver,
private ConstantConditionInTraitHelper $constantConditionInTraitHelper,
#[AutowiredParameter(ref: '%exceptions.reportUncheckedExceptionDeadCatch%')]
private bool $reportUncheckedExceptionDeadCatch,
)
Expand All @@ -31,44 +35,55 @@ public function __construct(

public function getNodeType(): string
{
return CatchWithUnthrownExceptionNode::class;
return CatchTypeNode::class;
}

public function processNode(Node $node, Scope $scope): array
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
{
if ($node->getCaughtType() instanceof NeverType) {
return [
RuleErrorBuilder::message(
sprintf('Dead catch - %s is already caught above.', $node->getOriginalCaughtType()->describe(VerbosityLevel::typeOnly())),
)
->line($node->getStartLine())
->identifier('catch.alreadyCaught')
->build(),
];
if ($node->isThrown()) {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getTypeNode());
return [];
}

if (!$this->reportUncheckedExceptionDeadCatch) {
$isCheckedException = false;
foreach ($node->getCaughtType()->getObjectClassNames() as $objectClassName) {
if ($this->exceptionTypeResolver->isCheckedException($objectClassName, $scope)) {
$isCheckedException = true;
break;
if ($node->getCaughtType() instanceof NeverType) {
$ruleError = RuleErrorBuilder::message(
sprintf('Dead catch - %s is already caught above.', $node->getOriginalCaughtType()->describe(VerbosityLevel::typeOnly())),
)
->line($node->getStartLine())
->identifier('catch.alreadyCaught')
->build();
} else {
if (!$this->reportUncheckedExceptionDeadCatch) {
$isCheckedException = false;
foreach ($node->getCaughtType()->getObjectClassNames() as $objectClassName) {
if ($this->exceptionTypeResolver->isCheckedException($objectClassName, $scope)) {
$isCheckedException = true;
break;
}
}
}

if (!$isCheckedException) {
return [];
if (!$isCheckedException) {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getTypeNode());
return [];
}
}
}

return [
RuleErrorBuilder::message(
$ruleError = RuleErrorBuilder::message(
sprintf('Dead catch - %s is never thrown in the try block.', $node->getCaughtType()->describe(VerbosityLevel::typeOnly())),
)
->line($node->getStartLine())
->identifier('catch.neverThrown')
->build(),
];
->build();
}

if ($scope->isInTrait()) {
// The message already distinguishes the possible outcomes,
// so the contexts only need to be told apart by error/no error.
$this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->getTypeNode(), true, $ruleError);
return [];
}

return [$ruleError];
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Rules\Exceptions;

use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;
Expand All @@ -21,7 +22,7 @@ protected function getRule(): Rule
[],
[],
[],
), true);
), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), true);
}

public function testRule(): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Rules\Exceptions;

use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

Expand All @@ -19,7 +20,7 @@ protected function getRule(): Rule
[],
[],
[],
), true);
), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), true);
}

public function testRuleInPhpVersionRange(): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Rules\Exceptions;

use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use const PHP_VERSION_ID;
Expand All @@ -20,7 +21,7 @@ protected function getRule(): Rule
[],
[],
[],
), true);
), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), true);
}

public function testThrowTypesInPhpVersionRange(): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Rules\Exceptions;

use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

Expand All @@ -19,7 +20,7 @@ protected function getRule(): Rule
[],
[],
[],
), true);
), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), true);
}

public function testGetClassThrowTypeInPhpVersionRange(): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Rules\Exceptions;

use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

Expand All @@ -19,7 +20,7 @@ protected function getRule(): Rule
[],
[],
[],
), true);
), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), true);
}

public function testRule(): void
Expand Down
Loading
Loading