Passed
Push — master ( 419306...682405 )
by Ross
02:29
created

TacticianRuleSet::getNodeType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
declare(strict_types=1);
3
4
namespace League\Tactician\PHPStan;
5
6
use League\Tactician\Handler\Mapping\CommandToHandlerMapping;
7
use PhpParser\Node;
8
use PhpParser\Node\Expr\MethodCall;
9
use PHPStan\Analyser\Scope;
10
use PHPStan\Broker\Broker;
11
use PHPStan\Broker\ClassNotFoundException;
12
use PHPStan\Reflection\MissingMethodFromReflectionException;
13
use PHPStan\Reflection\ParametersAcceptorSelector;
14
use PHPStan\Rules\Rule;
15
use PHPStan\Type\ObjectType;
16
use PHPStan\Type\Type;
17
use PHPStan\Type\TypeWithClassName;
18
use PHPStan\Type\UnionType;
19
use function array_filter;
20
use function array_merge;
21
22
final class TacticianRuleSet implements Rule
23
{
24
    /**
25
     * @var CommandToHandlerMapping
26
     */
27
    private $mapping;
28
    /**
29
     * @var Broker
30
     */
31
    private $broker;
32
    /**
33
     * @var string
34
     */
35
    private $commandBusClass;
36
    /**
37
     * @var string
38
     */
39
    private $commandBusMethod;
40
41 14
    public function __construct(
42
        CommandToHandlerMapping $mapping,
43
        Broker $broker,
44
        string $commandBusClass,
45
        string $commandBusMethod
46
    ) {
47 14
        $this->mapping = $mapping;
48 14
        $this->broker = $broker;
49 14
        $this->commandBusClass = $commandBusClass;
50 14
        $this->commandBusMethod = $commandBusMethod;
51 14
    }
52
53 14
    public function getNodeType(): string
54
    {
55 14
        return MethodCall::class;
56
    }
57
58 14
    public function processNode(Node $methodCall, Scope $scope): array
59
    {
60 14
        if (! $methodCall instanceof MethodCall
61 14
            || ! $methodCall->name instanceof Node\Identifier
62 14
            || $methodCall->name->name !== $this->commandBusMethod) {
63 1
            return [];
64
        }
65
66 13
        $type = $scope->getType($methodCall->var);
67
68 13
        if (! (new ObjectType($this->commandBusClass))->isSuperTypeOf($type)->yes()) {
69
            return [];
70
        }
71
72
        // Wrong number of arguments passed to handle? Delegate to other PHPStan rules
73 13
        if (count($methodCall->args) !== 1) {
74 1
            return []; //
75
        }
76
77 12
        $commandType = $scope->getType($methodCall->args[0]->value);
78
79 12
        $errors = [];
80 12
        foreach ($this->getInspectableCommandTypes($commandType) as $commandType) {
81 11
            $errors = array_merge(
82 11
                $errors,
83 11
                $this->inspectCommandType($methodCall, $scope, $commandType)
84
            );
85
        }
86
87 12
        return $errors;
88
    }
89
90
    /**
91
     * @return array<string>
92
     */
93 11
    private function inspectCommandType(
94
        MethodCall $methodCallOnBus,
95
        Scope $scope,
96
        TypeWithClassName $commandType
97
    ): array {
98 11
        $handlerClassName = $this->mapping->getClassName($commandType->getClassName());
99
100
        try {
101 11
            $handlerClass = $this->broker->getClass($handlerClassName);
102 2
        } catch (ClassNotFoundException $e) {
103
            return [
104 2
                "Tactician tried to route the command {$commandType->getClassName()} but could not find the matching " .
105 2
                "handler {$handlerClassName}.",
106
            ];
107
        }
108
109 10
        $handlerMethodName = $this->mapping->getMethodName($commandType->getClassName());
110
111
        try {
112 10
            $handlerMethod = $handlerClass->getMethod($handlerMethodName, $scope);
113 1
        } catch (MissingMethodFromReflectionException $e) {
114
            return [
115 1
                "Tactician tried to route the command {$commandType->getClassName()} to " .
116 1
                "{$handlerClass->getName()}::{$handlerMethodName} but while the class could be loaded, the method " .
117 1
                "'{$handlerMethodName}' could not be found on the class.",
118
            ];
119
        }
120
121
        /** @var \PHPStan\Reflection\ParameterReflection[] $parameters */
122 9
        $parameters = ParametersAcceptorSelector::selectFromArgs(
123 9
            $scope,
124 9
            $methodCallOnBus->args,
125 9
            $handlerMethod->getVariants()
126 9
        )->getParameters();
127
128 9
        if (count($parameters) === 0) {
129
            return [
130 2
                "Tactician tried to route the command {$commandType->getClassName()} to " .
131 2
                "{$handlerClass->getName()}::{$handlerMethodName} but the method '{$handlerMethodName}' does not " .
132 2
                "accept any parameters.",
133
            ];
134
        }
135
136 7
        if (count($parameters) > 1) {
137
            return [
138 1
                "Tactician tried to route the command {$commandType->getClassName()} to " .
139 1
                "{$handlerClass->getName()}::{$handlerMethodName} but the method '{$handlerMethodName}' accepts " .
140 1
                "too many parameters.",
141
            ];
142
        }
143
144 6
        if ($parameters[0]->getType()->accepts($commandType, true)->no()) {
145
            return [
146 2
                "Tactician tried to route the command {$commandType->getClassName()} to " .
147 2
                "{$handlerClass->getName()}::{$handlerMethodName} but the method '{$handlerMethodName}' has a " .
148 2
                "typehint that does not allow this command.",
149
            ];
150
        }
151
152 4
        return [];
153
    }
154
155
    /** @return TypeWithClassName[] */
156 12
    private function getInspectableCommandTypes(Type $type): array
157
    {
158 12
        $types = [];
159 12
        if ($type instanceof TypeWithClassName) {
160 11
            $types = [$type];
161
        }
162
163 12
        if ($type instanceof UnionType) {
164 1
            $types = $type->getTypes();
165
        }
166
167 12
        return array_filter(
168 12
            $types,
169
            function (Type $type) {
170 12
                if(! $type instanceof TypeWithClassName) {
171
                    return false;
172
                }
173
174 12
                $classReflection = $this->broker->getClass($type->getClassName());
175
176 12
                return ! ($classReflection->isInterface() || $classReflection->isAbstract());
177 12
            }
178
        );
179
    }
180
}
181