Passed
Push — main ( 87f70c...0bc0e4 )
by mikhail
06:42 queued 02:51
created

ExceptionChecker::isExceptionCaught()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 21
ccs 11
cts 11
cp 1
rs 9.2222
c 0
b 0
f 0
cc 6
nc 6
nop 1
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SavinMikhail\CommentsDensity\MissingDocblock\Visitors\Checkers;
6
7
use PhpParser\Node;
8
use PhpParser\Node\Expr\New_;
9
use PhpParser\Node\Expr\Throw_;
10
use PhpParser\Node\Expr\Variable;
11
use PhpParser\Node\Stmt\Catch_;
12
use PhpParser\Node\Stmt\TryCatch;
13
use ReflectionClass;
14
15
final class ExceptionChecker
16
{
17
    private array $tryCatchStack = [];
18
    public bool $hasUncaughtThrows = false;
19
20 10
    public function checkIfExceptionIsCaught(Throw_ $node): void
21
    {
22 10
        if (!$this->isInTryBlock($node)) {
23 4
            $this->hasUncaughtThrows = true;
24 7
        } elseif (!$this->isExceptionCaught($node)) {
25 2
            $this->hasUncaughtThrows = true;
26 5
        } elseif ($this->isInCatchBlock($node)) {
27
            $this->hasUncaughtThrows = true;
28
        }
29
    }
30
31 7
    public function pushTryCatch(TryCatch $node): void
32
    {
33 7
        $this->tryCatchStack[] = $node;
34
    }
35
36 7
    public function popTryCatch(): void
37
    {
38 7
        array_pop($this->tryCatchStack);
39
    }
40
41 5
    private function isInCatchBlock(Throw_ $node): bool
42
    {
43 5
        foreach ($this->getCurrentCatchStack() as $catch) {
44 5
            if ($this->nodeIsWithin($node, $catch)) {
45
                return true;
46
            }
47
        }
48 5
        return false;
49
    }
50
51 10
    private function isInTryBlock(Throw_ $node): bool
52
    {
53 10
        foreach ($this->tryCatchStack as $tryCatch) {
54 7
            foreach ($tryCatch->stmts as $stmt) {
55 7
                if ($this->nodeIsWithin($node, $stmt)) {
56 7
                    return true;
57
                }
58
            }
59
        }
60 4
        return false;
61
    }
62
63 7
    private function nodeIsWithin(Node $node, Node $container): bool
64
    {
65 7
        return $node->getStartFilePos() >= $container->getStartFilePos() &&
66 7
            $node->getEndFilePos() <= $container->getEndFilePos();
67
    }
68
69 7
    private function getExceptionFQN(Throw_ $throwNode): string
70
    {
71 7
        $throwExpr = $throwNode->expr;
72
73 7
        if ($throwExpr instanceof Variable) {
74 2
            $thrownExceptionType = $throwExpr->name;
75 5
        } elseif ($throwExpr instanceof New_) {
76 5
            $thrownExceptionType = $throwExpr->class->name;
0 ignored issues
show
Bug introduced by
Accessing name on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
77
        }
78
79 7
        if ($thrownExceptionType[0] !== '\\') {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $thrownExceptionType does not seem to be defined for all execution paths leading up to this point.
Loading history...
80 5
            $thrownExceptionType = '\\' . $thrownExceptionType;
81
        }
82
83 7
        return $thrownExceptionType;
84
    }
85
86 7
    private function isExceptionCaught(Throw_ $throwNode): bool
87
    {
88 7
        $thrownExceptionType = $this->getExceptionFQN($throwNode);
89
90 7
        foreach ($this->getCurrentCatchStack() as $catch) {
91 7
            foreach ($catch->types as $catchType) {
92 7
                $catchTypeName = $catchType->name;
93 7
                if (!$catchType->isQualified()) {
94 7
                    $catchTypeName = '\\' . $catchTypeName;
95
                }
96
97
                if (
98 7
                    $this->isSubclassOf($thrownExceptionType, $catchTypeName)
99 7
                    || $catchType->name === 'Throwable'
100
                ) {
101 5
                    return true;
102
                }
103
            }
104
        }
105
106 2
        return false;
107
    }
108
109
    /** @return Catch_[] */
110 7
    private function getCurrentCatchStack(): array
111
    {
112 7
        $catchStack = [];
113 7
        foreach ($this->tryCatchStack as $tryCatch) {
114 7
            $catchStack = array_merge($catchStack, $tryCatch->catches);
115
        }
116 7
        return $catchStack;
117
    }
118
119 7
    private function isSubclassOf(?string $className, string $parentClassName): bool
120
    {
121 7
        if ($className === null) {
122
            return false;
123
        }
124
125 7
        if (!class_exists($className) || !class_exists($parentClassName)) {
126 3
            return false;
127
        }
128
129 6
        $reflectionClass = new ReflectionClass($className);
130 6
        return $reflectionClass->isSubclassOf($parentClassName) || $className === $parentClassName;
131
    }
132
}
133