Passed
Push — main ( bdd3af...bc6954 )
by mikhail
03:49
created

ExceptionChecker::isSubclassOf()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.0729

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 12
ccs 6
cts 7
cp 0.8571
rs 9.6111
cc 5
nc 4
nop 2
crap 5.0729
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SavinMikhail\CommentsDensity\MissingDocblock\Visitors;
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) && !$this->isRethrowingCaughtException($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
    private function isRethrowingCaughtException(Throw_ $throwNode): bool
64
    {
65
        $throwExpr = $throwNode->expr;
66
67
        if (!$throwExpr instanceof Variable) {
68
            return false;
69
        }
70
71
        foreach ($this->getCurrentCatchStack() as $catch) {
72
            if ($catch->var instanceof Variable && $catch->var->name === $throwExpr->name) {
73
                return true;
74
            }
75
        }
76
77
        return false;
78
    }
79
80 7
    private function nodeIsWithin(Node $node, Node $container): bool
81
    {
82 7
        return $node->getStartFilePos() >= $container->getStartFilePos() &&
83 7
            $node->getEndFilePos() <= $container->getEndFilePos();
84
    }
85
86 7
    private function getExceptionFQN(Throw_ $throwNode): string
87
    {
88 7
        $throwExpr = $throwNode->expr;
89
90 7
        if ($throwExpr instanceof Variable) {
91 2
            $thrownExceptionType = $throwExpr->name;
92 5
        } elseif ($throwExpr instanceof New_) {
93 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...
94
        }
95
96 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...
97 5
            $thrownExceptionType = '\\' . $thrownExceptionType;
98
        }
99
100 7
        return $thrownExceptionType;
101
    }
102
103 7
    private function isExceptionCaught(Throw_ $throwNode): bool
104
    {
105 7
        $thrownExceptionType = $this->getExceptionFQN($throwNode);
106
107 7
        foreach ($this->getCurrentCatchStack() as $catch) {
108 7
            foreach ($catch->types as $catchType) {
109 7
                $catchTypeName = $catchType->name;
110 7
                if (!$catchType->isQualified()) {
111 7
                    $catchTypeName = '\\' . $catchTypeName;
112
                }
113
114
                if (
115 7
                    $this->isSubclassOf($thrownExceptionType, $catchTypeName)
116 7
                    || $catchType->name === 'Throwable'
117
                ) {
118 5
                    return true;
119
                }
120
            }
121
        }
122
123 2
        return false;
124
    }
125
126
    /** @return Catch_[] */
127 7
    private function getCurrentCatchStack(): array
128
    {
129 7
        $catchStack = [];
130 7
        foreach ($this->tryCatchStack as $tryCatch) {
131 7
            $catchStack = array_merge($catchStack, $tryCatch->catches);
132
        }
133 7
        return $catchStack;
134
    }
135
136 7
    private function isSubclassOf(?string $className, string $parentClassName): bool
137
    {
138 7
        if ($className === null) {
139
            return false;
140
        }
141
142 7
        if (!class_exists($className) || !class_exists($parentClassName)) {
143 3
            return false;
144
        }
145
146 6
        $reflectionClass = new ReflectionClass($className);
147 6
        return $reflectionClass->isSubclassOf($parentClassName) || $className === $parentClassName;
148
    }
149
}
150