Skip to content

Commit 6f96c7a

Browse files
Adding a rule to forbid catching of certain types of exceptions (#5)
1 parent 921226b commit 6f96c7a

File tree

6 files changed

+194
-6
lines changed

6 files changed

+194
-6
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Service;
6+
7+
class TestService
8+
{
9+
public function methodWithAllowedCatch(): void
10+
{
11+
try {
12+
// Some code that might throw an exception
13+
} catch (\InvalidArgumentException $e) {
14+
// This should NOT trigger an error (not in forbidden list)
15+
}
16+
17+
try {
18+
// Some code that might throw an exception
19+
} catch (\RuntimeException $e) {
20+
// This should NOT trigger an error (not in forbidden list)
21+
}
22+
23+
try {
24+
// Some code that might throw an exception
25+
} catch (\DomainException $e) {
26+
// This should NOT trigger an error (not in forbidden list)
27+
}
28+
}
29+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Service;
6+
7+
class TestService
8+
{
9+
public function methodWithForbiddenCatch(): void
10+
{
11+
try {
12+
// Some code that might throw an exception
13+
} catch (\Exception $e) {
14+
// This should trigger an error
15+
}
16+
17+
try {
18+
// Some code that might throw an error
19+
} catch (\Error $e) {
20+
// This should trigger an error
21+
}
22+
23+
try {
24+
// Some code that might throw a throwable
25+
} catch (\Throwable $e) {
26+
// This should trigger an error
27+
}
28+
29+
try {
30+
// Some code that might throw a specific exception
31+
} catch (\InvalidArgumentException $e) {
32+
// This should NOT trigger an error (not in forbidden list)
33+
}
34+
}
35+
}

docs/Rules.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,20 @@ Ensures that classes inside namespaces matching a given regex must have names ma
9999
- phpstan.rules.rule
100100
```
101101

102+
## Catch Exception of Type Not Allowed Rule
103+
104+
Ensures that specific exception types are not caught in catch blocks. This is useful for preventing the catching of overly broad exception types like `Exception`, `Error`, or `Throwable`.
105+
106+
**Configuration Example:**
107+
```neon
108+
-
109+
class: Phauthentic\PhpstanRules\Architecture\CatchExceptionOfTypeNotAllowedRule
110+
arguments:
111+
forbiddenExceptionTypes: ['Exception', 'Error', 'Throwable']
112+
tags:
113+
- phpstan.rules.rule
114+
```
115+
102116
## Method Signature Must Match Rule
103117

104118
Ensures that methods matching a class and method name pattern have a specific signature, including parameter types, names, and count.

phpstan.neon

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,40 @@ parameters:
77

88
services:
99
-
10-
class: Phauthentic\PhpstanRules\CleanCode\ControlStructureNestingRule
10+
class: Phauthentic\PHPStanRules\CleanCode\ControlStructureNestingRule
1111
arguments:
1212
maxNestingLevel: 2
1313
tags:
1414
- phpstan.rules.rule
1515
-
16-
class: Phauthentic\PhpstanRules\CleanCode\TooManyArgumentsRule
16+
class: Phauthentic\PHPStanRules\CleanCode\TooManyArgumentsRule
1717
arguments:
1818
maxArguments: 3
1919
tags:
2020
- phpstan.rules.rule
2121
-
22-
class: Phauthentic\PhpstanRules\Architecture\ReadonlyClassRule
22+
class: Phauthentic\PHPStanRules\Architecture\ClassMustBeReadonlyRule
2323
arguments:
2424
patterns: ['/^App\\Controller\\/']
2525
tags:
2626
- phpstan.rules.rule
2727
-
28-
class: Phauthentic\PhpstanRules\Architecture\DependencyConstraintsRule
28+
class: Phauthentic\PHPStanRules\Architecture\DependencyConstraintsRule
2929
arguments:
3030
forbiddenDependencies: [
3131
'/^App\\Domain(?:\\\w+)*$/': ['/^App\\Controller\\/']
3232
]
3333
tags:
3434
- phpstan.rules.rule
3535
-
36-
class: Phauthentic\PhpstanRules\Architecture\FinalClassRule
36+
class: Phauthentic\PHPStanRules\Architecture\ClassMustBeFinalRule
3737
arguments:
3838
patterns: ['/^App\\Service\\/']
3939
tags:
4040
- phpstan.rules.rule
4141
-
42-
class: Phauthentic\PhpstanRules\Architecture\NamespaceClassPatternRule
42+
class: Phauthentic\PHPStanRules\Architecture\ClassnameMustMatchPatternRule
43+
4344
arguments:
4445
namespaceClassPatterns: [
4546
[
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phauthentic\PHPStanRules\Architecture;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\Catch_;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
13+
/**
14+
* @implements Rule<Catch_>
15+
*/
16+
class CatchExceptionOfTypeNotAllowedRule implements Rule
17+
{
18+
private const ERROR_MESSAGE = 'Catching exception of type %s is not allowed.';
19+
20+
private const IDENTIFIER = 'phauthentic.architecture.catchExceptionOfTypeNotAllowed';
21+
22+
/**
23+
* @var array<string> An array of exception class names that are not allowed to be caught.
24+
* e.g., ['Exception', 'Error', 'Throwable']
25+
*/
26+
private array $forbiddenExceptionTypes;
27+
28+
/**
29+
* @param array<string> $forbiddenExceptionTypes An array of exception class names that are not allowed to be caught.
30+
*/
31+
public function __construct(array $forbiddenExceptionTypes)
32+
{
33+
$this->forbiddenExceptionTypes = $forbiddenExceptionTypes;
34+
}
35+
36+
public function getNodeType(): string
37+
{
38+
return Catch_::class;
39+
}
40+
41+
public function processNode(Node $node, Scope $scope): array
42+
{
43+
if (!$node instanceof Catch_) {
44+
return [];
45+
}
46+
47+
$errors = [];
48+
49+
foreach ($node->types as $type) {
50+
$exceptionType = $type->toString();
51+
52+
// Check if the caught exception type is in the forbidden list
53+
if (in_array($exceptionType, $this->forbiddenExceptionTypes, true)) {
54+
$errors[] = RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $exceptionType))
55+
->line($node->getLine())
56+
->identifier(self::IDENTIFIER)
57+
->build();
58+
}
59+
}
60+
61+
return $errors;
62+
}
63+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phauthentic\PHPStanRules\Tests\TestCases\Architecture;
6+
7+
use Phauthentic\PHPStanRules\Architecture\CatchExceptionOfTypeNotAllowedRule;
8+
use PHPStan\Testing\RuleTestCase;
9+
10+
/**
11+
* @extends RuleTestCase<CatchExceptionOfTypeNotAllowedRule>
12+
*/
13+
class CatchExceptionOfTypeNotAllowedRuleTest extends RuleTestCase
14+
{
15+
protected function getRule(): \PHPStan\Rules\Rule
16+
{
17+
return new CatchExceptionOfTypeNotAllowedRule([
18+
'Exception',
19+
'Error',
20+
'Throwable',
21+
]);
22+
}
23+
24+
public function testRule(): void
25+
{
26+
$this->analyse([__DIR__ . '/../../../data/CatchExceptionOfTypeNotAllowed/CatchForbiddenException.php'], [
27+
[
28+
'Catching exception of type Exception is not allowed.',
29+
13,
30+
],
31+
[
32+
'Catching exception of type Error is not allowed.',
33+
19,
34+
],
35+
[
36+
'Catching exception of type Throwable is not allowed.',
37+
25,
38+
],
39+
]);
40+
}
41+
42+
public function testAllowedExceptions(): void
43+
{
44+
$this->analyse([__DIR__ . '/../../../data/CatchExceptionOfTypeNotAllowed/CatchAllowedException.php'], []);
45+
}
46+
}

0 commit comments

Comments
 (0)