Completed
Push — master ( 34569c...ff013c )
by Erin
02:19
created

ExceptionMatcher::match()   C

Complexity

Conditions 7
Paths 24

Size

Total Lines 38
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 24
nc 24
nop 1
dl 0
loc 38
rs 6.7272
c 0
b 0
f 0
1
<?php
2
namespace Peridot\Leo\Matcher;
3
4
use Exception;
5
use Peridot\Leo\Matcher\Template\ArrayTemplate;
6
use Peridot\Leo\Matcher\Template\TemplateInterface;
7
use Throwable;
8
9
/**
10
 * ExceptionMatcher executes a callable and determines if an exception of a given type was thrown. It optionally
11
 * matches the exception message.
12
 *
13
 * @package Peridot\Leo\Matcher
14
 */
15
class ExceptionMatcher extends AbstractMatcher
16
{
17
    /**
18
     * @var array
19
     */
20
    protected $arguments = [];
21
22
    /**
23
     * @var string
24
     */
25
    protected $expectedMessage = "";
26
27
    /**
28
     * A captured exception message
29
     *
30
     * @var string $message
31
     */
32
    protected $message;
33
34
    /**
35
     * @var TemplateInterface
36
     */
37
    protected $messageTemplate;
38
39
    /**
40
     * @param callable $expected
0 ignored issues
show
Bug introduced by
There is no parameter named $expected. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
41
     */
42
    public function __construct($exceptionType)
43
    {
44
        $this->expected = $exceptionType;
45
    }
46
47
    /**
48
     * Set arguments to be passed to the callable.
49
     *
50
     * @param array $arguments
51
     * @return $this
52
     */
53
    public function setArguments(array $arguments)
54
    {
55
        $this->arguments = $arguments;
56
        return $this;
57
    }
58
59
    /**
60
     * Set the expected message of the exception.
61
     *
62
     * @param string $message
63
     * @return $this
64
     */
65
    public function setExpectedMessage($message)
66
    {
67
        $this->expectedMessage = $message;
68
        return $this;
69
    }
70
71
    /**
72
     * Set the message thrown from an exception resulting from the
73
     * callable being invoked.
74
     *
75
     * @param string $message
76
     */
77
    public function setMessage($message)
78
    {
79
        $this->message = $message;
80
    }
81
82
    /**
83
     * Returns the arguments passed to the callable.
84
     *
85
     * @return array
86
     */
87
    public function getArguments()
88
    {
89
        return $this->arguments;
90
    }
91
92
    /**
93
     * Return the expected exception message.
94
     *
95
     * @return string
96
     */
97
    public function getExpectedMessage()
98
    {
99
        return $this->expectedMessage;
100
    }
101
102
    /**
103
     * Return the message thrown by an exception resulting from the callable
104
     * being invoked.
105
     *
106
     * @return string
107
     */
108
    public function getMessage()
109
    {
110
        return $this->message;
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     *
116
     * If the expected message has been set, the message template will be used.
117
     *
118
     * @return TemplateInterface
119
     */
120
    public function getTemplate()
121
    {
122
        if ($this->expectedMessage) {
123
            return $this->getMessageTemplate();
124
        }
125
        return parent::getTemplate();
126
    }
127
128
    /**
129
     * Set the template to be used when an expected exception message is provided.
130
     *
131
     * @param TemplateInterface $template
132
     * @return $this
133
     */
134
    public function setMessageTemplate(TemplateInterface $template)
135
    {
136
        $this->messageTemplate = $template;
137
        return $this;
138
    }
139
140
    /**
141
     * Return a template for rendering exception message templates.
142
     *
143
     * return TemplateInterface
144
     */
145
    public function getMessageTemplate()
146
    {
147
        if ($this->messageTemplate) {
148
            return $this->messageTemplate;
149
        }
150
        return $this->getDefaultMessageTemplate();
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     *
156
     * @return TemplateInterface
157
     */
158
    public function getDefaultTemplate()
159
    {
160
        $template = new ArrayTemplate([
161
            'default' => 'Expected exception of type {{expected}}',
162
            'negated' => 'Expected type of exception not to be {{expected}}'
163
        ]);
164
165
        return $template;
166
    }
167
168
    /**
169
     * Return a default template for exception message assertions.
170
     *
171
     * @return ArrayTemplate
172
     */
173
    public function getDefaultMessageTemplate()
174
    {
175
        return new ArrayTemplate([
176
            'default' => 'Expected exception message {{expected}}, got {{actual}}',
177
            'negated' => 'Expected exception message {{actual}} not to equal {{expected}}'
178
        ]);
179
    }
180
181
    /**
182
     * Executes the callable and matches the exception type and exception message.
183
     *
184
     * @param $actual
185
     * @return Match
186
     */
187
    public function match($actual)
188
    {
189
        $this->validateCallable($actual);
190
        $exception = null;
191
192
        try {
193
            call_user_func_array($actual, $this->arguments);
194
            $isMatch = false;
0 ignored issues
show
Unused Code introduced by
$isMatch is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
195
        } catch (Exception $exception) {
196
            // fall-through ...
197
        } catch (Throwable $exception) {
198
            // fall-through ...
199
        }
200
201
        if ($exception) {
202
            $isMatch = $exception instanceof $this->expected;
203
            $message = $exception->getMessage();
0 ignored issues
show
Bug introduced by
The method getMessage does only exist in Exception, but not in Throwable.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
204
            $this->setMessage($message);
205
        } else {
206
            $isMatch = false;
207
        }
208
209
        if ($isMatch && $this->expectedMessage) {
210
            $isMatch = $message == $this->expectedMessage;
0 ignored issues
show
Bug introduced by
The variable $message does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
211
            $expected = $this->expectedMessage;
212
            $actual = $message;
213
        } else {
214
            $expected = $this->expected;
215
        }
216
217
        $isNegated = $this->isNegated();
218
219
        if ($isNegated) {
220
            $isMatch = !$isMatch;
221
        }
222
223
        return new Match($isMatch, $expected, $actual, $isNegated);
224
    }
225
226
    /**
227
     * Executes the callable and matches the exception type and exception message.
228
     *
229
     * @param $actual
230
     * @return bool
231
     */
232
    protected function doMatch($actual)
233
    {
234
        // unused
235
    }
236
237
    /**
238
     * Validate that expected is indeed a valid callable.
239
     *
240
     * @throws \BadFunctionCallException
241
     */
242
    protected function validateCallable($callable)
243
    {
244
        if (!is_callable($callable)) {
245
            $callable = rtrim(print_r($callable, true));
246
            throw new \BadFunctionCallException("Invalid callable " . $callable . " given");
247
        }
248
    }
249
}
250