Passed
Push — main ( 5aeeb8...5d6527 )
by mikhail
03:24
created

UncaughtExceptionVisitor::enterNode()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7.0368

Importance

Changes 6
Bugs 5 Features 0
Metric Value
eloc 10
c 6
b 5
f 0
dl 0
loc 16
ccs 10
cts 11
cp 0.9091
rs 8.8333
cc 7
nc 10
nop 1
crap 7.0368
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SavinMikhail\CommentsDensity\MissingDocblock;
6
7
use PhpParser\Node;
8
use PhpParser\Node\Expr\Throw_;
9
use PhpParser\Node\Expr\Variable;
10
use PhpParser\Node\Expr\New_;
11
use PhpParser\Node\Stmt\TryCatch;
12
use PhpParser\NodeVisitorAbstract;
13
use ReflectionClass;
14
15
final class UncaughtExceptionVisitor extends NodeVisitorAbstract
16
{
17
    public bool $hasUncaughtThrows = false;
18
    private array $tryCatchStack = [];
19
20 20
    public function enterNode(Node $node): null
21
    {
22 20
        if ($node instanceof TryCatch) {
23 5
            $this->tryCatchStack[] = $node;
24
        }
25
26 20
        if ($node instanceof Throw_) {
27 6
            if (!$this->isInTryBlock($node)) {
28 2
                $this->hasUncaughtThrows = true;
29 5
            } elseif (!$this->isExceptionCaught($node)) {
30 1
                $this->hasUncaughtThrows = true;
31 4
            } elseif ($this->isInCatchBlock($node) && !$this->isRethrowingCaughtException($node)) {
32
                $this->hasUncaughtThrows = true;
33
            }
34
        }
35 20
        return null;
36
    }
37
38 20
    public function leaveNode(Node $node): null
39
    {
40 20
        if ($node instanceof TryCatch) {
41 5
            array_pop($this->tryCatchStack);
42
        }
43
44 20
        return null;
45
    }
46
47 4
    private function isInCatchBlock(Throw_ $node): bool
48
    {
49 4
        foreach ($this->getCurrentCatchStack() as $catch) {
50 4
            if ($this->nodeIsWithin($node, $catch)) {
51
                return true;
52
            }
53
        }
54 4
        return false;
55
    }
56
57 6
    private function isInTryBlock(Throw_ $node): bool
58
    {
59 6
        foreach ($this->tryCatchStack as $tryCatch) {
60 5
            foreach ($tryCatch->stmts as $stmt) {
61 5
                if ($this->nodeIsWithin($node, $stmt)) {
62 5
                    return true;
63
                }
64
            }
65
        }
66 2
        return false;
67
    }
68
69
    private function isRethrowingCaughtException(Throw_ $throwNode): bool
70
    {
71
        $throwExpr = $throwNode->expr;
72
73
        if (!$throwExpr instanceof Variable || !isset($throwExpr->name)) {
74
            return false;
75
        }
76
77
        foreach ($this->getCurrentCatchStack() as $catch) {
78
            if ($catch->var instanceof Variable && $catch->var->name === $throwExpr->name) {
79
                return true;
80
            }
81
        }
82
83
        return false;
84
    }
85
86 5
    private function nodeIsWithin(Node $node, Node $container): bool
87
    {
88 5
        return $node->getStartFilePos() >= $container->getStartFilePos() &&
89 5
            $node->getEndFilePos() <= $container->getEndFilePos();
90
    }
91
92 5
    private function isExceptionCaught(Throw_ $throwNode): bool
93
    {
94 5
        $throwExpr = $throwNode->expr;
95
96 5
        if ($throwExpr instanceof Variable) {
97
            $thrownExceptionType = $this->getVariableType($throwExpr->name);
0 ignored issues
show
Bug introduced by
It seems like $throwExpr->name can also be of type PhpParser\Node\Expr; however, parameter $varName of SavinMikhail\CommentsDen...itor::getVariableType() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

97
            $thrownExceptionType = $this->getVariableType(/** @scrutinizer ignore-type */ $throwExpr->name);
Loading history...
98 5
        } elseif ($throwExpr instanceof New_) {
99 5
            $thrownExceptionType = (string)$throwExpr->class;
100
        } else {
101
            return false;
102
        }
103
104 5
        foreach ($this->getCurrentCatchStack() as $catch) {
105 5
            foreach ($catch->types as $catchType) {
106
                if (
107 5
                    $this->isSubclassOf($thrownExceptionType, (string)$catchType)
108 5
                    || (string)$catchType === 'Throwable'
109
                ) {
110 4
                    return true;
111
                }
112
            }
113
        }
114
115 1
        return false;
116
    }
117
118 5
    private function getCurrentCatchStack(): array
119
    {
120 5
        $catchStack = [];
121 5
        foreach ($this->tryCatchStack as $tryCatch) {
122 5
            $catchStack = array_merge($catchStack, $tryCatch->catches);
123
        }
124 5
        return $catchStack;
125
    }
126
127
    private function getVariableType(string $varName): ?string
128
    {
129
        return $varName;
130
    }
131
132 5
    private function isSubclassOf(?string $className, string $parentClassName): bool
133
    {
134 5
        if ($className === null) {
135
            return false;
136
        }
137
138 5
        if (!class_exists($className) || !class_exists($parentClassName)) {
139 3
            return false;
140
        }
141
142 3
        $reflectionClass = new ReflectionClass($className);
143 3
        return $reflectionClass->isSubclassOf($parentClassName) || $className === $parentClassName;
144
    }
145
}
146