Passed
Push — main ( 38dda7...ecc027 )
by mikhail
03:24
created

getMethodThrownExceptions()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.1647

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 15
c 2
b 0
f 0
dl 0
loc 27
ccs 13
cts 16
cp 0.8125
rs 9.4555
cc 5
nc 5
nop 2
crap 5.1647
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SavinMikhail\CommentsDensity\MissingDocblock;
6
7
use PhpParser\Node;
8
use PhpParser\Node\Expr\Assign;
9
use PhpParser\Node\Expr\MethodCall;
10
use PhpParser\Node\Expr\Throw_;
11
use PhpParser\Node\Expr\Variable;
12
use PhpParser\Node\Expr\New_;
13
use PhpParser\Node\Stmt\Catch_;
14
use PhpParser\Node\Stmt\Class_;
15
use PhpParser\Node\Stmt\ClassMethod;
16
use PhpParser\Node\Stmt\Expression;
17
use PhpParser\Node\Stmt\TryCatch;
18
use PhpParser\NodeVisitorAbstract;
19
use ReflectionClass;
20
use phpDocumentor\Reflection\DocBlockFactory;
21
22
use function array_pop;
23
use function class_exists;
24
use function explode;
25
use function implode;
26
27
final class UncaughtExceptionVisitor extends NodeVisitorAbstract
28
{
29
    public bool $hasUncaughtThrows = false;
30
    /**
31
     * @var TryCatch[]
32
     */
33
    private array $tryCatchStack = [];
34
35
    private array $variableTypes = [];
36
37 39
    public function __construct(private readonly ?Class_ $class)
38
    {
39 39
    }
40
41 39
    public function enterNode(Node $node): ?Node
42
    {
43 39
        if ($node instanceof ClassMethod) {
44 32
            if ($this->class) {
45
                // Register the type of $this
46 32
                $className = $this->class->namespacedName->toString();
0 ignored issues
show
Bug introduced by
The method toString() does not exist on null. ( Ignorable by Annotation )

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

46
                /** @scrutinizer ignore-call */ 
47
                $className = $this->class->namespacedName->toString();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
47 32
                $this->variableTypes['this'] = $className;
48
            }
49 32
            if ($node->getStmts()) {
50 30
                foreach ($node->getStmts() as $stmt) {
51 30
                    if ($stmt instanceof Expression && $stmt->expr instanceof Assign) {
52 6
                        $var = $stmt->expr->var;
53 6
                        $expr = $stmt->expr->expr;
54 6
                        if ($var instanceof Variable && $expr instanceof New_) {
55 6
                            $this->variableTypes[$var->name] = $expr->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...
56
                        }
57
                    }
58
                }
59
            }
60
        }
61
62 39
        if ($node instanceof TryCatch) {
63 7
            $this->tryCatchStack[] = $node;
64
        }
65
66 39
        if ($node instanceof Throw_) {
67 7
            if (!$this->isInTryBlock($node)) {
68 3
                $this->hasUncaughtThrows = true;
69 5
            } elseif (!$this->isExceptionCaught($node)) {
70 1
                $this->hasUncaughtThrows = true;
71 4
            } elseif ($this->isInCatchBlock($node) && !$this->isRethrowingCaughtException($node)) {
72
                $this->hasUncaughtThrows = true;
73
            }
74
        }
75
76 39
        if ($node instanceof MethodCall) {
77 7
            $methodName = $node->name->name;
78 7
            if (isset($node->var->name)) {
79 7
                $objectName = (string) $node->var->name;
80
            }
81
82 7
            if (isset($objectName)) {
83 7
                $class = $this->variableTypes[$objectName] ?? null;
84
85 7
                if ($class) {
86 5
                    $exceptions = $this->getMethodThrownExceptions($class, $methodName);
87 5
                    foreach ($exceptions as $exception) {
88 4
                        $throwNode = new Throw_(new Variable($exception), $node->getAttributes());
89
90 4
                        if (!$this->isInTryBlock($throwNode)) {
91 2
                            $this->hasUncaughtThrows = true;
92 2
                        } elseif (!$this->isExceptionCaught($throwNode)) {
93 1
                            $this->hasUncaughtThrows = true;
94 1
                        } elseif ($this->isInCatchBlock($throwNode) && !$this->isRethrowingCaughtException($throwNode)) {
95
                            $this->hasUncaughtThrows = true;
96
                        }
97
                    }
98
                }
99
            }
100
        }
101
102 39
        return null;
103
    }
104
105 5
    private function getMethodThrownExceptions(string $className, string $methodName): array
106
    {
107 5
        if (!class_exists($className)) {
108
            return [];
109
        }
110
111 5
        $reflectionClass = new ReflectionClass($className);
112 5
        if (!$reflectionClass->hasMethod($methodName)) {
113
            return [];
114
        }
115
116 5
        $reflectionMethod = $reflectionClass->getMethod($methodName);
117 5
        $docComment = $reflectionMethod->getDocComment();
118
119 5
        if (!$docComment) {
120
            return [];
121
        }
122
123 5
        $docBlockFactory = DocBlockFactory::createInstance();
124 5
        $docBlock = $docBlockFactory->create($docComment);
125
126 5
        $exceptions = [];
127 5
        foreach ($docBlock->getTagsByName('throws') as $tag) {
128 4
            $exceptions[] = (string)$tag->getType();
129
        }
130
131 5
        return $exceptions;
132
    }
133
134 39
    public function leaveNode(Node $node): ?Node
135
    {
136 39
        if ($node instanceof TryCatch) {
137 7
            array_pop($this->tryCatchStack);
138
        }
139
140 39
        return null;
141
    }
142
143 5
    private function isInCatchBlock(Throw_ $node): bool
144
    {
145 5
        foreach ($this->getCurrentCatchStack() as $catch) {
146 5
            if ($this->nodeIsWithin($node, $catch)) {
147
                return true;
148
            }
149
        }
150 5
        return false;
151
    }
152
153 10
    private function isInTryBlock(Throw_ $node): bool
154
    {
155 10
        foreach ($this->tryCatchStack as $tryCatch) {
156 7
            foreach ($tryCatch->stmts as $stmt) {
157 7
                if ($this->nodeIsWithin($node, $stmt)) {
158 7
                    return true;
159
                }
160
            }
161
        }
162 4
        return false;
163
    }
164
165
    private function isRethrowingCaughtException(Throw_ $throwNode): bool
166
    {
167
        $throwExpr = $throwNode->expr;
168
169
        if (!$throwExpr instanceof Variable) {
170
            return false;
171
        }
172
173
        foreach ($this->getCurrentCatchStack() as $catch) {
174
            if ($catch->var instanceof Variable && $catch->var->name === $throwExpr->name) {
175
                return true;
176
            }
177
        }
178
179
        return false;
180
    }
181
182 7
    private function nodeIsWithin(Node $node, Node $container): bool
183
    {
184 7
        return $node->getStartFilePos() >= $container->getStartFilePos() &&
185 7
            $node->getEndFilePos() <= $container->getEndFilePos();
186
    }
187
188 7
    private function isExceptionCaught(Throw_ $throwNode): bool
189
    {
190 7
        $throwExpr = $throwNode->expr;
191
192 7
        if ($throwExpr instanceof Variable) {
193 2
            $thrownExceptionType = $throwExpr->name;
194 5
        } elseif ($throwExpr instanceof New_) {
195 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...
196
        } else {
197
            return false;
198
        }
199
200 7
        foreach ($this->getCurrentCatchStack() as $catch) {
201 7
            foreach ($catch->types as $catchType) {
202 7
                $catchTypeName = $catchType->name;
203 7
                if (!$catchType->isQualified()) {
204 7
                    $catchTypeName = '\\' . $catchTypeName;
205
                }
206 7
                if ($thrownExceptionType[0] !== '\\') {
207 5
                    $thrownExceptionType = '\\' . $thrownExceptionType;
208
                }
209
                if (
210 7
                    $this->isSubclassOf($thrownExceptionType, $catchTypeName)
211 7
                    || $catchType->name === 'Throwable'
212
                ) {
213 5
                    return true;
214
                }
215
            }
216
        }
217
218 2
        return false;
219
    }
220
221
    /** @return Catch_[] */
222 7
    private function getCurrentCatchStack(): array
223
    {
224 7
        $catchStack = [];
225 7
        foreach ($this->tryCatchStack as $tryCatch) {
226 7
            $catchStack = array_merge($catchStack, $tryCatch->catches);
227
        }
228 7
        return $catchStack;
229
    }
230
231 7
    private function isSubclassOf(?string $className, string $parentClassName): bool
232
    {
233 7
        if ($className === null) {
234
            return false;
235
        }
236
237 7
        if (!class_exists($className) || !class_exists($parentClassName)) {
238 3
            return false;
239
        }
240
241 6
        $reflectionClass = new ReflectionClass($className);
242 6
        return $reflectionClass->isSubclassOf($parentClassName) || $className === $parentClassName;
243
    }
244
}
245