Passed
Push — master ( 1173f0...070f3f )
by Nate
13:41
created

JsonElementReader::push()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 11
Ratio 100 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
dl 11
loc 11
ccs 8
cts 8
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 2
crap 2
1
<?php
2
/*
3
 * Copyright (c) Nate Brunette.
4
 * Distributed under the MIT License (http://opensource.org/licenses/MIT)
5
 */
6
7
namespace Tebru\Gson\Internal;
8
9
use ArrayIterator;
10
use SebastianBergmann\CodeCoverage\Node\Iterator;
11
use Tebru\Gson\Element\JsonArray;
12
use Tebru\Gson\Element\JsonElement;
13
use Tebru\Gson\Element\JsonNull;
14
use Tebru\Gson\Element\JsonObject;
15
use Tebru\Gson\Element\JsonPrimitive;
16
use Tebru\Gson\Exception\UnexpectedJsonTokenException;
17
use Tebru\Gson\JsonReadable;
18
use Tebru\Gson\JsonToken;
19
20
/**
21
 * Class JsonElementReader
22
 *
23
 * @author Nate Brunette <[email protected]>
24
 */
25
final class JsonElementReader implements JsonReadable
26
{
27
    /**
28
     * A stack representing the next element to be consumed
29
     *
30
     * @var array
31
     */
32
    private $stack = [];
33
34
    /**
35
     * An array of types that map to the position in the stack
36
     *
37
     * @var array
38
     */
39
    private $stackTypes = [];
40
41
    /**
42
     * The current size of the stack
43
     *
44
     * @var int
45
     */
46
    private $stackSize = 0;
47
48
    /**
49
     * A cache of the current [@see JsonToken].  This should get nulled out
50
     * whenever a new token should be returned with the subsequent call
51
     * to [@see JsonDecodeReader::peek]
52
     *
53
     * @var
54
     */
55
    private $currentToken;
56
57
    /**
58
     * Constructor
59
     *
60
     * @param JsonElement $jsonElement
61
     */
62 69
    public function __construct(JsonElement $jsonElement)
63
    {
64 69
        $this->push($jsonElement);
65 69
    }
66
67
    /**
68
     * Consumes the next token and asserts it's the beginning of a new array
69
     *
70
     * @return void
71
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not BEGIN_ARRAY
72
     */
73 15
    public function beginArray(): void
74
    {
75 15
        if ($this->peek() !== JsonToken::BEGIN_ARRAY) {
76 1
            throw new UnexpectedJsonTokenException(
77 1
                sprintf('Expected "%s", but found "%s"', JsonToken::BEGIN_ARRAY, $this->peek())
78
            );
79
        }
80
81
        /** @var JsonArray $jsonArray */
82 14
        $jsonArray = $this->pop();
83 14
        $this->push($jsonArray->getIterator(), ArrayIterator::class);
0 ignored issues
show
Documentation introduced by
$jsonArray->getIterator() is of type object<ArrayIterator>, but the function expects a object<Tebru\Gson\Elemen...Coverage\Node\Iterator>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
84 14
    }
85
86
    /**
87
     * Consumes the next token and asserts it's the end of an array
88
     *
89
     * @return void
90
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not END_ARRAY
91
     */
92 5
    public function endArray(): void
93
    {
94 5
        if ($this->peek() !== JsonToken::END_ARRAY) {
95 1
            throw new UnexpectedJsonTokenException(
96 1
                sprintf('Expected "%s", but found "%s"', JsonToken::END_ARRAY, $this->peek())
97
            );
98
        }
99
100 4
        $this->pop();
101 4
    }
102
103
    /**
104
     * Consumes the next token and asserts it's the beginning of a new object
105
     *
106
     * @return void
107
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not BEGIN_OBJECT
108
     */
109 17
    public function beginObject(): void
110
    {
111 17
        if ($this->peek() !== JsonToken::BEGIN_OBJECT) {
112 1
            throw new UnexpectedJsonTokenException(
113 1
                sprintf('Expected "%s", but found "%s"', JsonToken::BEGIN_OBJECT, $this->peek())
114
            );
115
        }
116
117 16
        $this->push(new JsonObjectIterator($this->pop()->asJsonObject()), JsonObjectIterator::class);
0 ignored issues
show
Bug introduced by
The method asJsonObject does only exist in Tebru\Gson\Element\JsonElement, but not in SebastianBergmann\CodeCoverage\Node\Iterator.

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...
Documentation introduced by
new \Tebru\Gson\Internal...>pop()->asJsonObject()) is of type object<Tebru\Gson\Internal\JsonObjectIterator>, but the function expects a object<Tebru\Gson\Elemen...Coverage\Node\Iterator>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
118 16
    }
119
120
    /**
121
     * Consumes the next token and asserts it's the end of an object
122
     *
123
     * @return void
124
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not END_OBJECT
125
     */
126 4
    public function endObject(): void
127
    {
128 4
        if ($this->peek() !== JsonToken::END_OBJECT) {
129 1
            throw new UnexpectedJsonTokenException(
130 1
                sprintf('Expected "%s", but found "%s"', JsonToken::END_OBJECT, $this->peek())
131
            );
132
        }
133
134 3
        $this->pop();
135 3
    }
136
137
    /**
138
     * Returns true if the array or object has another element
139
     *
140
     * If the current scope is not an array or object, this returns false
141
     *
142
     * @return bool
143
     */
144 4
    public function hasNext(): bool
145
    {
146 4
        $peek = $this->peek();
147
148 4
        return $peek !== JsonToken::END_OBJECT && $peek !== JsonToken::END_ARRAY;
149
    }
150
151
    /**
152
     * Consumes the value of the next token, asserts it's a boolean and returns it
153
     *
154
     * @return bool
155
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not BOOLEAN
156
     */
157 6
    public function nextBoolean(): bool
158
    {
159 6
        if ($this->peek() !== JsonToken::BOOLEAN) {
160 1
            throw new UnexpectedJsonTokenException(
161 1
                sprintf('Expected "%s", but found "%s"', JsonToken::BOOLEAN, $this->peek())
162
            );
163
        }
164
165 5
        return $this->pop()->asBoolean();
0 ignored issues
show
Bug introduced by
The method asBoolean does only exist in Tebru\Gson\Element\JsonElement, but not in SebastianBergmann\CodeCoverage\Node\Iterator.

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...
166
    }
167
168
    /**
169
     * Consumes the value of the next token, asserts it's a double and returns it
170
     *
171
     * @return double
172
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NUMBER
173
     */
174 3 View Code Duplication
    public function nextDouble(): float
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
175
    {
176 3
        if ($this->peek() !== JsonToken::NUMBER) {
177 1
            throw new UnexpectedJsonTokenException(
178 1
                sprintf('Expected "%s", but found "%s"', JsonToken::NUMBER, $this->peek())
179
            );
180
        }
181
182 2
        return $this->pop()->asFloat();
0 ignored issues
show
Bug introduced by
The method asFloat does only exist in Tebru\Gson\Element\JsonElement, but not in SebastianBergmann\CodeCoverage\Node\Iterator.

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...
183
    }
184
185
    /**
186
     * Consumes the value of the next token, asserts it's an int and returns it
187
     *
188
     * @return int
189
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NUMBER
190
     */
191 6
    public function nextInteger(): int
192
    {
193 6
        if ($this->peek() !== JsonToken::NUMBER) {
194 1
            throw new UnexpectedJsonTokenException(
195 1
                sprintf('Expected "%s", but found "%s"', JsonToken::NUMBER, $this->peek())
196
            );
197
        }
198
199 5
        return $this->pop()->asInteger();
0 ignored issues
show
Bug introduced by
The method asInteger does only exist in Tebru\Gson\Element\JsonElement, but not in SebastianBergmann\CodeCoverage\Node\Iterator.

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...
200
    }
201
202
    /**
203
     * Consumes the value of the next token, asserts it's a string and returns it
204
     *
205
     * @return string
206
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NAME or STRING
207
     */
208 14 View Code Duplication
    public function nextString(): string
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
209
    {
210 14
        $peek = $this->peek();
211 14
        if ($peek === JsonToken::NAME) {
212 1
            return $this->nextName();
213
        }
214
215 13
        if ($peek !== JsonToken::STRING) {
216 1
            throw new UnexpectedJsonTokenException(
217 1
                sprintf('Expected "%s", but found "%s"', JsonToken::STRING, $this->peek())
218
            );
219
        }
220
221 12
        return $this->pop()->asString();
0 ignored issues
show
Bug introduced by
The method asString does only exist in Tebru\Gson\Element\JsonElement, but not in SebastianBergmann\CodeCoverage\Node\Iterator.

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...
222
    }
223
224
    /**
225
     * Consumes the value of the next token and asserts it's null
226
     *
227
     * @return null
228
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NAME or NULL
229
     */
230 2 View Code Duplication
    public function nextNull()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
231
    {
232 2
        if ($this->peek() !== JsonToken::NULL) {
233 1
            throw new UnexpectedJsonTokenException(
234 1
                sprintf('Expected "%s", but found "%s"', JsonToken::NULL, $this->peek())
235
            );
236
        }
237
238 1
        $this->pop();
239
240 1
        return null;
241
    }
242
243
    /**
244
     * Consumes the next name and returns it
245
     *
246
     * @return string
247
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NAME
248
     */
249 9 View Code Duplication
    public function nextName(): string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
250
    {
251 9
        if ($this->peek() !== JsonToken::NAME) {
252 1
            throw new UnexpectedJsonTokenException(
253 1
                sprintf('Expected "%s", but found "%s"', JsonToken::NAME, $this->peek())
254
            );
255
        }
256
257
        /** @var JsonObjectIterator $iterator */
258 9
        $iterator = $this->stack[$this->stackSize - 1];
259 9
        $key = $iterator->key();
260 9
        $value = $iterator->current();
261 9
        $iterator->next();
262
263 9
        $this->push($value);
264
265 9
        return $key;
266
    }
267
268
    /**
269
     * Returns an enum representing the type of the next token without consuming it
270
     *
271
     * @return string
272
     */
273 69
    public function peek(): string
274
    {
275 69
        if (null !== $this->currentToken) {
276 11
            return $this->currentToken;
277
        }
278
279 69
        if (0 === $this->stackSize) {
280 1
            $this->currentToken = JsonToken::END_DOCUMENT;
281
282 1
            return $this->currentToken;
283
        }
284
285 69
        $token = null;
286 69
        $element = $this->stack[$this->stackSize - 1];
287
288 69
        switch ($this->stackTypes[$this->stackSize - 1]) {
289 69
            case JsonArray::class:
290 16
                $token = JsonToken::BEGIN_ARRAY;
291 16
                break;
292 66
            case JsonNull::class:
293 2
                $token = JsonToken::NULL;
294 2
                break;
295 64
            case JsonObject::class:
296 20
                $token = JsonToken::BEGIN_OBJECT;
297 20
                break;
298 61
            case JsonPrimitive::class:
299 45
                if ($element->isString()) {
300 18
                    $token = JsonToken::STRING;
301 28
                } elseif ($element->isBoolean()) {
302 8
                    $token = JsonToken::BOOLEAN;
303 21
                } elseif ($element->isNumber()) {
304 21
                    $token = JsonToken::NUMBER;
305
                }
306
307 45
                break;
308 27
            case JsonObjectIterator::class:
309 15
                $token = $element->valid() ? JsonToken::NAME : JsonToken::END_OBJECT;
310
311 15
                break;
312 13 View Code Duplication
            case ArrayIterator::class:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
313 13
                if ($element->valid()) {
314 9
                    $this->push($element->current());
315 9
                    $element->next();
316
317 9
                    $token = $this->peek();
318
                } else {
319 7
                    $token = JsonToken::END_ARRAY;
320
                }
321
322 13
                break;
323
        }
324
325 69
        $this->currentToken = $token;
326
327 69
        return $this->currentToken;
328
    }
329
330
    /**
331
     * Skip the next value.  If the next value is an object or array, all children will
332
     * also be skipped.
333
     *
334
     * @return void
335
     */
336 1
    public function skipValue(): void
337
    {
338 1
        $this->pop();
339 1
    }
340
341
    /**
342
     * Push an element onto the stack
343
     *
344
     * @param JsonElement|Iterator $element
345
     * @param string $type
0 ignored issues
show
Documentation introduced by
Should the type for parameter $type not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
346
     */
347 69 View Code Duplication
    private function push($element, $type = null): void
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
348
    {
349 69
        if (null === $type) {
350 69
            $type = get_class($element);
1 ignored issue
show
Coding Style introduced by
Consider using a different name than the parameter $type. This often makes code more readable.
Loading history...
351
        }
352
353 69
        $this->stack[$this->stackSize] = $element;
354 69
        $this->stackTypes[$this->stackSize] = $type;
355 69
        $this->stackSize++;
356 69
        $this->currentToken = null;
357 69
    }
358
359
    /**
360
     * Pop the last element off the stack and return it
361
     *
362
     * @return JsonElement|Iterator
363
     */
364 45 View Code Duplication
    private function pop()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
365
    {
366 45
        $this->stackSize--;
367 45
        array_pop($this->stackTypes);
368 45
        $this->currentToken = null;
369
370 45
        return array_pop($this->stack);
371
    }
372
}
373