Test Failed
Push — main ( 46eee6...5832e5 )
by mikhail
03:28
created

ExceptionChecker   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 133
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 38
eloc 58
c 1
b 0
f 0
dl 0
loc 133
rs 9.36

11 Methods

Rating   Name   Duplication   Size   Complexity  
A nodeIsWithin() 0 4 2
A popTryCatch() 0 3 1
A getCurrentCatchStack() 0 7 2
A getExceptionFQN() 0 15 4
A isInTryBlock() 0 10 4
A isSubclassOf() 0 12 5
A isRethrowingCaughtException() 0 15 5
A isInCatchBlock() 0 8 3
A isExceptionCaught() 0 21 6
A checkIfExceptionIsCaught() 0 8 5
A pushTryCatch() 0 3 1
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
    public function checkIfExceptionIsCaught(Throw_ $node): void
21
    {
22
        if (!$this->isInTryBlock($node)) {
23
            $this->hasUncaughtThrows = true;
24
        } elseif (!$this->isExceptionCaught($node)) {
25
            $this->hasUncaughtThrows = true;
26
        } elseif ($this->isInCatchBlock($node) && !$this->isRethrowingCaughtException($node)) {
27
            $this->hasUncaughtThrows = true;
28
        }
29
    }
30
31
    public function pushTryCatch(TryCatch $node): void
32
    {
33
        $this->tryCatchStack[] = $node;
34
    }
35
36
    public function popTryCatch(): void
37
    {
38
        array_pop($this->tryCatchStack);
39
    }
40
41
    private function isInCatchBlock(Throw_ $node): bool
42
    {
43
        foreach ($this->getCurrentCatchStack() as $catch) {
44
            if ($this->nodeIsWithin($node, $catch)) {
45
                return true;
46
            }
47
        }
48
        return false;
49
    }
50
51
    private function isInTryBlock(Throw_ $node): bool
52
    {
53
        foreach ($this->tryCatchStack as $tryCatch) {
54
            foreach ($tryCatch->stmts as $stmt) {
55
                if ($this->nodeIsWithin($node, $stmt)) {
56
                    return true;
57
                }
58
            }
59
        }
60
        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
    private function nodeIsWithin(Node $node, Node $container): bool
81
    {
82
        return $node->getStartFilePos() >= $container->getStartFilePos() &&
83
            $node->getEndFilePos() <= $container->getEndFilePos();
84
    }
85
86
    private function getExceptionFQN(Throw_ $throwNode): string
87
    {
88
        $throwExpr = $throwNode->expr;
89
90
        if ($throwExpr instanceof Variable) {
91
            $thrownExceptionType = $throwExpr->name;
92
        } elseif ($throwExpr instanceof New_) {
93
            $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
        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
            $thrownExceptionType = '\\' . $thrownExceptionType;
98
        }
99
100
        return $thrownExceptionType;
101
    }
102
103
    private function isExceptionCaught(Throw_ $throwNode): bool
104
    {
105
        $thrownExceptionType = $this->getExceptionFQN($throwNode);
106
107
        foreach ($this->getCurrentCatchStack() as $catch) {
108
            foreach ($catch->types as $catchType) {
109
                $catchTypeName = $catchType->name;
110
                if (!$catchType->isQualified()) {
111
                    $catchTypeName = '\\' . $catchTypeName;
112
                }
113
114
                if (
115
                    $this->isSubclassOf($thrownExceptionType, $catchTypeName)
116
                    || $catchType->name === 'Throwable'
117
                ) {
118
                    return true;
119
                }
120
            }
121
        }
122
123
        return false;
124
    }
125
126
    /** @return Catch_[] */
127
    private function getCurrentCatchStack(): array
128
    {
129
        $catchStack = [];
130
        foreach ($this->tryCatchStack as $tryCatch) {
131
            $catchStack = array_merge($catchStack, $tryCatch->catches);
132
        }
133
        return $catchStack;
134
    }
135
136
    private function isSubclassOf(?string $className, string $parentClassName): bool
137
    {
138
        if ($className === null) {
139
            return false;
140
        }
141
142
        if (!class_exists($className) || !class_exists($parentClassName)) {
143
            return false;
144
        }
145
146
        $reflectionClass = new ReflectionClass($className);
147
        return $reflectionClass->isSubclassOf($parentClassName) || $className === $parentClassName;
148
    }
149
}
150