ExpressionLexer::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 5
c 1
b 1
f 0
dl 0
loc 7
ccs 6
cts 6
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace POData\UriProcessor\QueryProcessor\ExpressionParser;
4
5
use POData\Common\Messages;
6
use POData\Common\ODataException;
7
use POData\Common\ODataConstants;
8
use POData\Providers\Metadata\Type\Char;
9
10
/**
11
 * Class ExpressionLexer
12
 *
13
 * Lexical analyzer for Astoria URI expression parsing
14
 * Literals        Representation
15
 * --------------------------------------------------------------------
16
 * Null            null
17
 * Boolean         true | false
18
 * Int32           (digit+)
19
 * Int64           (digit+)(L|l)
20
 * Decimal         (digit+ ['.' digit+])(M|m)
21
 * Float (Single)  (digit+ ['.' digit+][e|E [+|-] digit+)(f|F)
22
 * Double          (digit+ ['.' digit+][e|E [+|-] digit+)
23
 * String          "'" .* "'"
24
 * DateTime        datetime"'"dddd-dd-dd[T|' ']dd:mm[ss[.fffffff]]"'"
25
 * Binary          (binary|X)'digit*'
26
 * GUID            guid'digit*
27
 *
28
 * @package POData\UriProcessor\QueryProcessor\ExpressionParser
29
 */
30
class ExpressionLexer
31
{
32
    /**
33
     * Suffix for single literals
34
     *
35
     * @var char
36
     */
37
    const SINGLE_SUFFIX_LOWER = 'f';
38
39
    /**
40
     * Suffix for single literals
41
     *
42
     * @var char
43
     */
44
    const SINGLE_SUFFIX_UPPER = 'F';
45
46
    /**
47
     * Text being parsed
48
     *
49
     * @var string
50
     */
51
    private $_text;
52
53
    /**
54
     * Length of text being parsed
55
     *
56
     * @var int
57
     */
58
    private $_textLen;
59
60
    /**
61
     * Position on text being parsed
62
     *
63
     * @var int
64
     */
65
    private $_textPos;
66
67
    /**
68
     * Character being processed
69
     *
70
     * @var char
71
     */
72
    private $_ch;
73
74
    /**
75
     * ExpressionToken being processed
76
     *
77
     * @var ExpressionToken
78
     */
79
    private $_token;
80
81
    /**
82
     * Initialize a new instance of ExpressionLexer
83
     *
84
     * @param string $expression Expression to parse
85
     */
86 148
    public function __construct($expression)
87
    {
88 148
        $this->_text = $expression;
89 148
        $this->_textLen = strlen($this->_text);
90 148
        $this->_token = new ExpressionToken();
91 148
        $this->_setTextPos(0);
92 148
        $this->nextToken();
93
    }
94
95
    /**
96
     * To get the expression token being processed
97
     *
98
     * @return ExpressionToken
99
     */
100 148
    public function getCurrentToken()
101
    {
102 148
        return $this->_token;
103
    }
104
105
    /**
106
     * To set the token being processed
107
     *
108
     * @param ExpressionToken $token The expression token to set as current
109
     *
110
     * @return void
111
     */
112
    public function setCurrentToken($token)
113
    {
114
        $this->_token = $token;
115
    }
116
117
    /**
118
     * To get the text being parsed
119
     *
120
     * @return string
121
     */
122
    public function getExpressionText()
123
    {
124
        return $this->_text;
125
    }
126
127
    /**
128
     * Position of the current token in the text being parsed
129
     *
130
     * @return int
131
     */
132
    public function getPosition()
133
    {
134
        return $this->_token->Position;
135
    }
136
137
    /**
138
     * Whether the specified token identifier is a numeric literal
139
     *
140
     * @param ExpressionTokenId $id Token identifier to check
141
     *
142
     * @return bool true if it's a numeric literal; false otherwise
143
     */
144 41
    public static function isNumeric($id)
145
    {
146 41
        return
147 41
            $id == ExpressionTokenId::INTEGER_LITERAL
148 41
            || $id == ExpressionTokenId::DECIMAL_LITERAL
149 41
            || $id == ExpressionTokenId::DOUBLE_LITERAL
150 41
            || $id == ExpressionTokenId::INT64_LITERAL
151 41
            || $id == ExpressionTokenId::SINGLE_LITERAL;
152
    }
153
154
    /**
155
     * Reads the next token, skipping whitespace as necessary.
156
     *
157
     * @return void
158
     */
159 148
    public function nextToken()
160
    {
161
162 148
        while (Char::isWhiteSpace($this->_ch)) {
163 107
            $this->_nextChar();
164
        }
165
166 148
        $t = null;
167 148
        $tokenPos = $this->_textPos;
168 148
        $rest = substr($this->_text, $tokenPos);
169 148
        $matches_array = [];
170 148
        preg_match('/^\(\s*(?<string_array>(\'[^\']*\'(\s*\,\s*\'[^\']*\')*)|(?<int_array>\d+(\s*\,\s*\d+)*))\s*\)/', $rest, $matches_array);
171 148
        if ($matches_array) {
172
            $t = ExpressionTokenId::ARRAY_LITERAL;
173
            $this->_setTextPos($this->_textPos + strlen($matches_array[0]));
174
        } else {
175 148
            switch ($this->_ch) {
176 148
                case '(':
177 40
                    $this->_nextChar();
178 40
                    $t = ExpressionTokenId::OPENPARAM;
179 40
                    break;
180 148
                case ')':
181 40
                    $this->_nextChar();
182 40
                    $t = ExpressionTokenId::CLOSEPARAM;
183 40
                    break;
184 148
                case ',':
185 74
                    $this->_nextChar();
186 74
                    $t = ExpressionTokenId::COMMA;
187 74
                    break;
188 148
                case '-':
189 5
                    $hasNext = $this->_textPos + 1 < $this->_textLen;
190 5
                    if ($hasNext && Char::isDigit($this->_text[$this->_textPos + 1])) {
191 4
                        $this->_nextChar();
192 4
                        $t = $this->_parseFromDigit();
193 4
                        if (self::isNumeric($t)) {
194 4
                            break;
195
                        }
196
197
                        $this->_setTextPos($tokenPos);
198 4
                    } else if ($hasNext && $this->_text[$tokenPos + 1] == 'I') {
199 1
                        $this->_nextChar();
200 1
                        $this->_parseIdentifier();
201 1
                        $currentIdentifier = substr($this->_text, $tokenPos + 1, $this->_textPos - $tokenPos - 1);
202
203 1
                        if (self::_isInfinityLiteralDouble($currentIdentifier)) {
204 1
                            $t = ExpressionTokenId::DOUBLE_LITERAL;
205 1
                            break;
206 1
                        } else if (self::_isInfinityLiteralSingle($currentIdentifier)) {
207 1
                            $t = ExpressionTokenId::SINGLE_LITERAL;
208 1
                            break;
209
                        }
210
211
                        // If it looked like '-INF' but wasn't we'll rewind and fall
212
                        // through to a simple '-' token.
213
                        $this->_setTextPos($tokenPos);
214
                    }
215
216 4
                    $this->_nextChar();
217 4
                    $t = ExpressionTokenId::MINUS;
218 4
                    break;
219 148
                case '=':
220 18
                    $this->_nextChar();
221 18
                    $t = ExpressionTokenId::EQUAL;
222 18
                    break;
223 148
                case '/':
224 24
                    $this->_nextChar();
225 24
                    $t = ExpressionTokenId::SLASH;
226 24
                    break;
227 148
                case '?':
228
                    $this->_nextChar();
229
                    $t = ExpressionTokenId::QUESTION;
230
                    break;
231 148
                case '.':
232
                    $this->_nextChar();
233
                    $t = ExpressionTokenId::DOT;
234
                    break;
235 148
                case '\'':
236 66
                    $quote = $this->_ch;
237
                    do {
238 66
                        $this->_nextChar();
239 66
                        while ($this->_textPos < $this->_textLen && $this->_ch != $quote) {
240 66
                            $this->_nextChar();
241
                        }
242
243 66
                        if ($this->_textPos == $this->_textLen) {
244 1
                            $this->_parseError(
245 1
                                Messages::expressionLexerUnterminatedStringLiteral(
246 1
                                    $this->_textPos, $this->_text
247 1
                                )
248 1
                            );
249
                        }
250
251 66
                        $this->_nextChar();
252 66
                    } while ($this->_ch == $quote);
253 66
                    $t = ExpressionTokenId::STRING_LITERAL;
254 66
                    break;
255 148
                case '*':
256 5
                    $this->_nextChar();
257 5
                    $t = ExpressionTokenId::STAR;
258 5
                    break;
259
                default:
260 147
                    if (Char::isLetter($this->_ch) || $this->_ch == '_') {
261 135
                        $this->_parseIdentifier();
262 135
                        $t = ExpressionTokenId::IDENTIFIER;
263 135
                        break;
264
                    }
265
266 142
                    if (Char::isDigit($this->_ch)) {
267 66
                        $t = $this->_parseFromDigit();
268 66
                        break;
269
                    }
270
271 139
                    if ($this->_textPos == $this->_textLen) {
272 138
                        $t = ExpressionTokenId::END;
273 138
                        break;
274
                    }
275
276 2
                    $this->_parseError(
277 2
                        Messages::expressionLexerInvalidCharacter(
278 2
                            $this->_ch, $this->_textPos
0 ignored issues
show
Bug introduced by
$this->_ch of type POData\Providers\Metadata\Type\Char is incompatible with the type string expected by parameter $ch of POData\Common\Messages::...LexerInvalidCharacter(). ( Ignorable by Annotation )

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

278
                            /** @scrutinizer ignore-type */ $this->_ch, $this->_textPos
Loading history...
279 2
                        )
280 2
                    );
281
            }
282
        }
283
284 148
        $this->_token->Id = $t;
0 ignored issues
show
Documentation Bug introduced by
It seems like $t can also be of type integer. However, the property $Id is declared as type POData\UriProcessor\Quer...arser\ExpressionTokenId. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
285 148
        $this->_token->Text = substr($this->_text, $tokenPos, $this->_textPos - $tokenPos);
286 148
        $this->_token->Position = $tokenPos;
287
288
        // Handle type-prefixed literals such as binary, datetime or guid.
289 148
        $this->_handleTypePrefixedLiterals();
290
291
        // Handle keywords.
292 148
        if ($this->_token->Id == ExpressionTokenId::IDENTIFIER) {
293 134
            if (self::_isInfinityOrNaNDouble($this->_token->Text)) {
294 1
                $this->_token->Id = ExpressionTokenId::DOUBLE_LITERAL;
295 134
            } else if (self::_isInfinityOrNanSingle($this->_token->Text)) {
296 1
                $this->_token->Id = ExpressionTokenId::SINGLE_LITERAL;
297 134
            } else if ($this->_token->Text == ODataConstants::KEYWORD_TRUE
298 134
                || $this->_token->Text == ODataConstants::KEYWORD_FALSE
299
            ) {
300 8
                $this->_token->Id = ExpressionTokenId::BOOLEAN_LITERAL;
301 134
            } else if ($this->_token->Text == ODataConstants::KEYWORD_NULL) {
302 11
                $this->_token->Id = ExpressionTokenId::NULL_LITERAL;
303
            }
304
        }
305
    }
306
307
    /**
308
     * Returns the next token without advancing the lexer to next token
309
     *
310
     * @return ExpressionToken
311
     */
312 56
    public function peekNextToken()
313
    {
314 56
        $savedTextPos = $this->_textPos;
315 56
        $savedChar = $this->_ch;
316 56
        $savedToken = clone $this->_token;
317 56
        $this->nextToken();
318 56
        $result = clone $this->_token;
319 56
        $this->_textPos = $savedTextPos;
320 56
        $this->_ch = $savedChar;
321 56
        $this->_token->Id = $savedToken->Id;
322 56
        $this->_token->Position = $savedToken->Position;
323 56
        $this->_token->Text = $savedToken->Text;
324 56
        return $result;
325
    }
326
327
    /**
328
     * Validates the current token is of the specified kind
329
     *
330
     * @param ExpressionTokenId $tokenId Expected token kind
331
     *
332
     * @return void
333
     *
334
     * @throws ODataException if current token is not of the
335
     *                        specified kind.
336
     */
337 98
    public function validateToken($tokenId)
338
    {
339 98
        if ($this->_token->Id != $tokenId) {
340 4
            $this->_parseError(
341 4
                Messages::expressionLexerSyntaxError(
342 4
                    $this->_textPos
343 4
                )
344 4
            );
345
        }
346
    }
347
348
    /**
349
     * Starting from an identifier, reads alternate sequence of dots and identifiers
350
     * and returns the text for it
351
     *
352
     * @return string The dotted identifier starting at the current identifier
353
     */
354 97
    public function readDottedIdentifier()
355
    {
356 97
        $this->validateToken(ExpressionTokenId::IDENTIFIER);
0 ignored issues
show
Bug introduced by
POData\UriProcessor\Quer...sionTokenId::IDENTIFIER of type integer is incompatible with the type POData\UriProcessor\Quer...arser\ExpressionTokenId expected by parameter $tokenId of POData\UriProcessor\Quer...nLexer::validateToken(). ( Ignorable by Annotation )

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

356
        $this->validateToken(/** @scrutinizer ignore-type */ ExpressionTokenId::IDENTIFIER);
Loading history...
357 96
        $identifier = $this->_token->Text;
358 96
        $this->nextToken();
359 96
        while ($this->_token->Id == ExpressionTokenId::DOT) {
360
            $this->nextToken();
361
            $this->validateToken(ExpressionTokenId::IDENTIFIER);
362
            $identifier = $identifier . '.' . $this->_token->Text;
363
            $this->nextToken();
364
        }
365
366 96
        return $identifier;
367
    }
368
369
    /**
370
     * Check if the parameter ($tokenText) is INF or NaN
371
     *
372
     * @param string $tokenText Text to look in
373
     *
374
     * @return boolean true if match found, false otherwise
375
     */
376 134
    private static function _isInfinityOrNaNDouble($tokenText)
377
    {
378 134
        if (strlen($tokenText) == 3) {
379 28
            if ($tokenText[0] == 'I') {
380 1
                return self::_isInfinityLiteralDouble($tokenText);
381 28
            } else if ($tokenText[0] == 'N') {
382 1
                return strncmp($tokenText, ODataConstants::XML_NAN_LITERAL, 3) == 0;
383
            }
384
        }
385
386 134
        return false;
387
    }
388
389
    /**
390
     * Check if the parameter ($text) is INF
391
     *
392
     * @param string $text Text to look in
393
     *
394
     * @return boolean true if match found, false otherwise
395
     */
396 2
    private static function _isInfinityLiteralDouble($text)
397
    {
398 2
        return strcmp($text, ODataConstants::XML_INFINITY_LITERAL) == 0;
399
    }
400
401
    /**
402
     * Checks if the parameter ($tokenText) is INFf/INFF or NaNf/NaNF.
403
     *
404
     * @param string $tokenText Input token
405
     *
406
     * @return bool true if match found, false otherwise
407
     */
408 134
    private static function _isInfinityOrNanSingle($tokenText)
409
    {
410 134
        if (strlen($tokenText) == 4) {
411 27
            if ($tokenText[0] == 'I') {
412 1
                return self::_isInfinityLiteralSingle($tokenText);
413 27
            } else if ($tokenText[0] == 'N') {
414 1
                return ($tokenText[3] == ExpressionLexer::SINGLE_SUFFIX_LOWER
415 1
                    || $tokenText[3] == ExpressionLexer::SINGLE_SUFFIX_UPPER)
416 1
                    && strncmp($tokenText, ODataConstants::XML_NAN_LITERAL, 3) == 0;
417
            }
418
        }
419
420 134
        return false;
421
    }
422
423
    /**
424
     * Checks whether parameter ($text) EQUALS to 'INFf' or 'INFF' at position
425
     *
426
     * @param string $text Text to look in
427
     *
428
     * @return bool true if the substring is equal using an ordinal comparison;
429
     *         false otherwise
430
     */
431 2
    private static function _isInfinityLiteralSingle($text)
432
    {
433 2
        return strlen($text) == 4
434 2
            && ($text[3] == ExpressionLexer::SINGLE_SUFFIX_LOWER
435 2
            || $text[3] == ExpressionLexer::SINGLE_SUFFIX_UPPER)
436 2
            && strncmp($text, ODataConstants::XML_INFINITY_LITERAL, 3) == 0;
437
    }
438
439
    /**
440
     * Handles the literals that are prefixed by types.
441
     * This method modified the token field as necessary.
442
     *
443
     * @return void
444
     *
445
     * @throws ODataException
446
     */
447 148
    private function _handleTypePrefixedLiterals()
448
    {
449 148
        $id = $this->_token->Id;
450 148
        if ($id != ExpressionTokenId::IDENTIFIER) {
0 ignored issues
show
introduced by
The condition $id != POData\UriProcess...sionTokenId::IDENTIFIER is always true.
Loading history...
451 144
            return;
452
        }
453
454 135
        $quoteFollows = $this->_ch == '\'';
455 135
        if (!$quoteFollows) {
456 134
            return;
457
        }
458
459 30
        $tokenText = $this->_token->Text;
460
461 30
        if (strcasecmp('datetime', $tokenText) == 0) {
462 12
            $id = ExpressionTokenId::DATETIME_LITERAL;
463 20
        } else if (strcasecmp('guid', $tokenText) == 0) {
464 20
            $id = ExpressionTokenId::GUID_LITERAL;
465 1
        } else if (strcasecmp('binary', $tokenText) == 0
466 1
            || strcasecmp('X', $tokenText) == 0
467 1
            || strcasecmp('x', $tokenText) == 0
468
        ) {
469 1
            $id = ExpressionTokenId::BINARY_LITERAL;
470
        } else {
471
            return;
472
        }
473
474 30
        $tokenPos = $this->_token->Position;
475
        do {
476 30
            $this->_nextChar();
477 30
        } while ($this->_ch != '\0' && $this->_ch != '\'');
478
479 30
        if ($this->_ch == '\0') {
480 1
            $this->_parseError(
481 1
                Messages::expressionLexerUnterminatedStringLiteral(
482 1
                    $this->_textPos, $this->_text
483 1
                )
484 1
            );
485
        }
486
487 30
        $this->_nextChar();
488 30
        $this->_token->Id = $id;
489 30
        $this->_token->Text
490 30
            = substr($this->_text, $tokenPos, $this->_textPos - $tokenPos);
491
    }
492
493
    /**
494
     * Parses a token that starts with a digit
495
     *
496
     * @return ExpressionTokenId The kind of token recognized.
497
     */
498 69
    private function _parseFromDigit()
499
    {
500 69
        $result = null;
501 69
        $startChar = $this->_ch;
502 69
        $this->_nextChar();
503 69
        if ($startChar == '0' && $this->_ch == 'x' || $this->_ch == 'X') {
0 ignored issues
show
introduced by
The condition $startChar == '0' is always false.
Loading history...
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($startChar == '0' && $t...') || $this->_ch == 'X', Probably Intended Meaning: $startChar == '0' && ($t...' || $this->_ch == 'X')
Loading history...
504
            $result = ExpressionTokenId::BINARY_LITERAL;
505
            do {
506
                $this->_nextChar();
507
            } while (ctype_xdigit($this->_ch));
508
        } else {
509 69
            $result = ExpressionTokenId::INTEGER_LITERAL;
510 69
            while (Char::isDigit($this->_ch)) {
0 ignored issues
show
Bug introduced by
It seems like $this->_ch can also be of type string; however, parameter $char of POData\Providers\Metadata\Type\Char::isDigit() does only seem to accept POData\Providers\Metadata\Type\char, 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

510
            while (Char::isDigit(/** @scrutinizer ignore-type */ $this->_ch)) {
Loading history...
511 53
                $this->_nextChar();
512
            }
513
514 69
            if ($this->_ch == '.') {
515 14
                $result = ExpressionTokenId::DOUBLE_LITERAL;
516 14
                $this->_nextChar();
517 14
                $this->_validateDigit();
518
519
                do {
520 14
                    $this->_nextChar();
521 14
                } while (Char::isDigit($this->_ch));
522
            }
523
524 69
            if ($this->_ch == 'E' || $this->_ch == 'e') {
525 2
                $result = ExpressionTokenId::DOUBLE_LITERAL;
526 2
                $this->_nextChar();
527 2
                if ($this->_ch == '+' || $this->_ch == '-') {
528 1
                    $this->_nextChar();
529
                }
530
531 2
                $this->_validateDigit();
532
                do {
533 2
                    $this->_nextChar();
534 2
                } while (Char::isDigit($this->_ch));
535
            }
536
537 69
            if ($this->_ch == 'M' || $this->_ch == 'm') {
538 4
                $result = ExpressionTokenId::DECIMAL_LITERAL;
539 4
                $this->_nextChar();
540 69
            } else if ($this->_ch == 'd' || $this->_ch == 'D') {
541 2
                $result = ExpressionTokenId::DOUBLE_LITERAL;
542 2
                $this->_nextChar();
543 69
            } else if ($this->_ch == 'L' || $this->_ch == 'l') {
544 3
                $result = ExpressionTokenId::INT64_LITERAL;
545 3
                $this->_nextChar();
546 69
            } else if ($this->_ch == 'f' || $this->_ch == 'F') {
547 5
                $result = ExpressionTokenId::SINGLE_LITERAL;
548 5
                $this->_nextChar();
549
            }
550
        }
551
552 69
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type integer which is incompatible with the documented return type POData\UriProcessor\Quer...arser\ExpressionTokenId.
Loading history...
553
    }
554
555
    /**
556
     * Parses an identifier by advancing the current character.
557
     *
558
     * @return void
559
     */
560 135
    private function _parseIdentifier()
561
    {
562
        do {
563 135
            $this->_nextChar();
564 135
        } while (Char::isLetterOrDigit($this->_ch) || $this->_ch == '_');
0 ignored issues
show
Bug introduced by
It seems like $this->_ch can also be of type string; however, parameter $char of POData\Providers\Metadat...Char::isLetterOrDigit() does only seem to accept POData\Providers\Metadata\Type\char, 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

564
        } while (Char::isLetterOrDigit(/** @scrutinizer ignore-type */ $this->_ch) || $this->_ch == '_');
Loading history...
565
    }
566
567
    /**
568
     * Advance to next character.
569
     *
570
     * @return void
571
     */
572 148
    private function _nextChar()
573
    {
574 148
        if ($this->_textPos < $this->_textLen) {
575 148
            $this->_textPos++;
576
        }
577
578 148
        $this->_ch
579 148
            = $this->_textPos < $this->_textLen
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->_textPos < $this-...$this->_textPos] : '\0' can also be of type string. However, the property $_ch is declared as type POData\Providers\Metadata\Type\Char. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
580 148
             ? $this->_text[$this->_textPos] : '\0';
581
    }
582
583
    /**
584
     * Set the text position.
585
     *
586
     * @param int $pos Value to position.
587
     *
588
     * @return void
589
     */
590 148
    private function _setTextPos($pos)
591
    {
592 148
        $this->_textPos = $pos;
593 148
        $this->_ch
594 148
            = $this->_textPos < $this->_textLen
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->_textPos < $this-...$this->_textPos] : '\0' can also be of type string. However, the property $_ch is declared as type POData\Providers\Metadata\Type\Char. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
595 148
             ? $this->_text[$this->_textPos] : '\0';
596
    }
597
598
    /**
599
     * Validate current character is a digit.
600
     *
601
     * @return void
602
     */
603 14
    private function _validateDigit()
604
    {
605 14
        if (!Char::isDigit($this->_ch)) {
606 1
            $this->_parseError(
607 1
                Messages::expressionLexerDigitExpected(
608 1
                    $this->_textPos
609 1
                )
610 1
            );
611
        }
612
    }
613
614
    /**
615
     * Throws parser error.
616
     *
617
     * @param string $message The error message.
618
     *
619
     * @return void
620
     *
621
     * @throws ODataException
622
     */
623 9
    private function _parseError($message)
624
    {
625 9
        throw ODataException::createSyntaxError($message);
626
    }
627
}
628