Passed
Push — main ( 30dfbd...b8dd52 )
by mikhail
03:24
created

UncaughtExceptionVisitor::getCurrentCatchStack()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 2
eloc 4
c 1
b 1
f 0
nc 2
nop 0
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 2
rs 10
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 11
    public function enterNode(Node $node): void
21
    {
22 11
        if ($node instanceof TryCatch) {
23 5
            $this->tryCatchStack[] = $node;
24
        }
25
26 11
        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
    }
36
37 11
    public function leaveNode(Node $node): void
38
    {
39 11
        if ($node instanceof TryCatch) {
40 5
            array_pop($this->tryCatchStack);
41
        }
42
    }
43
44 4
    private function isInCatchBlock(Node $node): bool
45
    {
46 4
        foreach ($this->getCurrentCatchStack() as $catch) {
47 4
            if ($this->nodeIsWithin($node, $catch)) {
48
                return true;
49
            }
50
        }
51 4
        return false;
52
    }
53
54 6
    private function isInTryBlock(Node $node): bool
55
    {
56 6
        foreach ($this->tryCatchStack as $tryCatch) {
57 5
            foreach ($tryCatch->stmts as $stmt) {
58 5
                if ($this->nodeIsWithin($node, $stmt)) {
59 5
                    return true;
60
                }
61
            }
62
        }
63 2
        return false;
64
    }
65
66
    private function isRethrowingCaughtException(Node $throwNode): bool
67
    {
68
        $throwExpr = $throwNode->expr;
0 ignored issues
show
Bug introduced by
Accessing expr on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
69
70
        if (!$throwExpr instanceof Variable || !isset($throwExpr->name)) {
71
            return false;
72
        }
73
74
        foreach ($this->getCurrentCatchStack() as $catch) {
75
            if ($catch->var instanceof Variable && $catch->var->name === $throwExpr->name) {
76
                return true;
77
            }
78
        }
79
80
        return false;
81
    }
82
83 5
    private function nodeIsWithin(Node $node, Node $container): bool
84
    {
85 5
        return $node->getStartFilePos() >= $container->getStartFilePos() &&
86 5
            $node->getEndFilePos() <= $container->getEndFilePos();
87
    }
88
89 5
    private function isExceptionCaught(Node $throwNode): bool
90
    {
91 5
        $throwExpr = $throwNode->expr;
0 ignored issues
show
Bug introduced by
Accessing expr on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
92
93 5
        if ($throwExpr instanceof Variable) {
94
            $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

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