ExpressionParser   F
last analyzed

Complexity

Total Complexity 94

Size/Duplication

Total Lines 742
Duplicated Lines 0 %

Test Coverage

Coverage 85.81%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 94
eloc 283
c 3
b 0
f 0
dl 0
loc 742
ccs 248
cts 289
cp 0.8581
rs 2

28 Methods

Rating   Name   Duplication   Size   Complexity  
A _parsePropertyAccess() 0 36 3
B _parseUnary() 0 35 6
A _parseLogicalAnd() 0 14 2
A _setCurrentToken() 0 3 1
C _parsePrimaryStart() 0 36 15
A _parseNullLiteral() 0 4 1
A _parseArgumentList() 0 16 4
A _parseComparison() 0 16 2
A _parseArguments() 0 13 3
A _parseLogicalOr() 0 16 2
A parseFilter() 0 3 1
A _validateToken() 0 4 2
A _parseIdentifierAsFunction() 0 10 1
A _parseIdentifier() 0 10 2
A _parseExpression() 0 6 1
A _getCurrentToken() 0 3 1
F _generateComparisonExpression() 0 120 20
A __construct() 0 6 1
A _parseAdditive() 0 19 4
A _parseParenExpression() 0 22 5
A resetParser() 0 4 1
A _tokenIdentifierIs() 0 3 1
A _recurseLeave() 0 3 1
A _parseMultiplicative() 0 25 6
A _recurseEnter() 0 5 2
A _parsePrimary() 0 15 3
A _parseTypedLiteral() 0 17 2
A hasLevel2Property() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like ExpressionParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ExpressionParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace POData\UriProcessor\QueryProcessor\ExpressionParser;
4
5
use POData\Common\Messages;
6
use POData\Common\ODataConstants;
7
use POData\Common\ODataException;
8
use POData\Providers\Metadata\Type\Boolean;
9
use POData\Providers\Metadata\Type\DateTime;
10
use POData\Providers\Metadata\Type\Decimal;
11
use POData\Providers\Metadata\Type\StringType;
12
use POData\Providers\Metadata\Type\Int64;
13
use POData\Providers\Metadata\Type\Int32;
14
use POData\Providers\Metadata\Type\Double;
15
use POData\Providers\Metadata\Type\Single;
16
use POData\Providers\Metadata\Type\Guid;
17
use POData\Providers\Metadata\Type\Binary;
18
use POData\Providers\Metadata\Type\NullType;
19
use POData\Providers\Metadata\Type\ArrayType;
20
use POData\Providers\Metadata\Type\IType;
21
use POData\Providers\Metadata\ResourceType;
22
use POData\Providers\Metadata\ResourcePropertyKind;
23
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\AbstractExpression;
24
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\PropertyAccessExpression;
25
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\ArithmeticExpression;
26
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\RelationalExpression;
27
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\LogicalExpression;
28
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\FunctionCallExpression;
29
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\UnaryExpression;
30
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\ConstantExpression;
31
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\ExpressionType;
32
use POData\UriProcessor\QueryProcessor\FunctionDescription;
33
use POData\Common\NotImplementedException;
34
35
36
/**
37
 * Class ExpressionParser
38
 * @package POData\UriProcessor\QueryProcessor\ExpressionParser
39
 */
40
class ExpressionParser
41
{
42
    const RECURSION_LIMIT = 200;
43
44
    /**
45
     * The Lexical analyzer
46
     *
47
     * @var ExpressionLexer
48
     */
49
    private $_lexer;
50
51
    /**
52
     * The current recursion depth
53
     *
54
     * @var int
55
     */
56
    private $_recursionDepth;
57
58
    /**
59
     * The ResourceType on which $filter condition needs to be applied
60
     *
61
     * @var ResourceType
62
     */
63
    private $_resourceType;
64
65
    /**
66
     * @var bool
67
     */
68
    private $_isPHPExpressionProvider;
69
70
    /**
71
     * True if the filter expression contains level 2 property access, for example
72
     * Customers?$filter=Address/LineNumber eq 12
73
     * Customer?$filter=Order/OrderID gt 1234
74
     * False otherwise.
75
     *
76
     * @var bool
77
     */
78
    private $_hasLevel2PropertyInTheExpression;
79
80
81
    /**
82
     * Construct a new instance of ExpressionParser
83
     *
84
     * @param string $text The expression to parse.
85
     * @param ResourceType $resourceType The resource type of the resource targeted by the resource path.
86
     * @param bool $isPHPExpressionProvider
87
     *
88
     * TODO Expression parser should not depend on the fact that end user is implementing IExpressionProvider or not.
89
     */
90 57
    public function __construct($text, ResourceType $resourceType, $isPHPExpressionProvider)
91
    {
92 57
        $this->_lexer = new ExpressionLexer($text);
93 57
        $this->_resourceType = $resourceType;
94 57
        $this->_isPHPExpressionProvider = $isPHPExpressionProvider;
95 57
        $this->_hasLevel2PropertyInTheExpression = false;
96
    }
97
98
    /**
99
     * Checks whether the expression contains level 2 property access.
100
     *
101
     * @return boolean
102
     */
103
    public function hasLevel2Property()
104
    {
105
        return $this->_hasLevel2PropertyInTheExpression;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_hasLevel2PropertyInTheExpression returns the type boolean which is incompatible with the documented return type POData\Providers\Metadata\Type\Boolean.
Loading history...
106
    }
107
108
    /**
109
     * Get the current token from lexer
110
     *
111
     * @return ExpressionToken
112
     */
113 57
    private function _getCurrentToken()
114
    {
115 57
        return $this->_lexer->getCurrentToken();
116
    }
117
118
    /**
119
     * Set the current token in lexer
120
     *
121
     * @param ExpressionToken $token The token to set as current token.
122
     *
123
     * @return void
124
     */
125
    private function _setCurrentToken($token)
126
    {
127
        $this->_lexer->setCurrentToken($token);
128
    }
129
130
    /**
131
     * Resets parser with new expression string.
132
     *
133
     * @param string $text Reset the expression to parse.
134
     *
135
     * @return void
136
     */
137 11
    public function resetParser($text)
138
    {
139 11
        $this->_lexer = new ExpressionLexer($text);
140 11
        $this->_recursionDepth = 0;
141
    }
142
143
    /**
144
     * Parse the expression in filter option.
145
     *
146
     * @return AbstractExpression
147
     */
148 57
    public function parseFilter()
149
    {
150 57
        return $this->_parseExpression();
151
    }
152
153
    /**
154
     * Start parsing the expression
155
     *
156
     * @return AbstractExpression
157
     */
158 57
    private function _parseExpression()
159
    {
160 57
        $this->_recurseEnter();
161 57
        $expr = $this->_parseLogicalOr();
162 57
        $this->_recurseLeave();
163 57
        return $expr;
164
    }
165
166
    /**
167
     * Parse logical or (or)
168
     *
169
     * @return AbstractExpression
170
     */
171 57
    private function _parseLogicalOr()
172
    {
173 57
        $this->_recurseEnter();
174 57
        $left = $this->_parseLogicalAnd();
175 57
        while ($this->_tokenIdentifierIs(ODataConstants::KEYWORD_OR)) {
0 ignored issues
show
Bug introduced by
POData\Common\ODataConstants::KEYWORD_OR of type string is incompatible with the type POData\UriProcessor\Quer...arser\ExpressionTokenId expected by parameter $expressionTokenId of POData\UriProcessor\Quer...r::_tokenIdentifierIs(). ( Ignorable by Annotation )

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

175
        while ($this->_tokenIdentifierIs(/** @scrutinizer ignore-type */ ODataConstants::KEYWORD_OR)) {
Loading history...
176 3
            $logicalOpToken = clone $this->_getCurrentToken();
177 3
            $this->_lexer->nextToken();
178 3
            $right = $this->_parseLogicalAnd();
179 3
            FunctionDescription::verifyLogicalOpArguments($logicalOpToken, $left, $right);
180 3
            $left = new LogicalExpression(
181 3
                $left, $right, ExpressionType::OR_LOGICAL
0 ignored issues
show
Bug introduced by
POData\UriProcessor\Quer...ressionType::OR_LOGICAL of type integer is incompatible with the type POData\UriProcessor\Quer...ressions\ExpressionType expected by parameter $nodeType of POData\UriProcessor\Quer...pression::__construct(). ( Ignorable by Annotation )

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

181
                $left, $right, /** @scrutinizer ignore-type */ ExpressionType::OR_LOGICAL
Loading history...
182 3
            );
183
        }
184
185 57
        $this->_recurseLeave();
186 57
        return $left;
187
    }
188
189
    /**
190
     * Parse logical and (and).
191
     *
192
     * @return AbstractExpression
193
     */
194 57
    private function _parseLogicalAnd()
195
    {
196 57
        $this->_recurseEnter();
197 57
        $left = $this->_parseComparison();
198 57
        while ($this->_tokenIdentifierIs(ODataConstants::KEYWORD_AND)) {
0 ignored issues
show
Bug introduced by
POData\Common\ODataConstants::KEYWORD_AND of type string is incompatible with the type POData\UriProcessor\Quer...arser\ExpressionTokenId expected by parameter $expressionTokenId of POData\UriProcessor\Quer...r::_tokenIdentifierIs(). ( Ignorable by Annotation )

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

198
        while ($this->_tokenIdentifierIs(/** @scrutinizer ignore-type */ ODataConstants::KEYWORD_AND)) {
Loading history...
199 6
            $logicalOpToken = clone $this->_getCurrentToken();
200 6
            $this->_lexer->nextToken();
201 6
            $right = $this->_parseComparison();
202 6
            FunctionDescription::verifyLogicalOpArguments($logicalOpToken, $left, $right);
203 6
            $left = new LogicalExpression($left, $right, ExpressionType::AND_LOGICAL);
0 ignored issues
show
Bug introduced by
POData\UriProcessor\Quer...essionType::AND_LOGICAL of type integer is incompatible with the type POData\UriProcessor\Quer...ressions\ExpressionType expected by parameter $nodeType of POData\UriProcessor\Quer...pression::__construct(). ( Ignorable by Annotation )

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

203
            $left = new LogicalExpression($left, $right, /** @scrutinizer ignore-type */ ExpressionType::AND_LOGICAL);
Loading history...
204
        }
205
206 57
        $this->_recurseLeave();
207 57
        return $left;
208
    }
209
210
    /**
211
     * Parse comparison operation (eq, ne, gt, ge, lt, le)
212
     * TODO: http://host/service/Products?$filter=Name in ('Milk', 'Cheese')
213
     * TODO: http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html
214
     * @return AbstractExpression
215
     */
216 57
    private function _parseComparison()
217
    {
218 57
        $this->_recurseEnter();
219 57
        $left = $this->_parseAdditive();
220 57
        while ($this->_getCurrentToken()->isComparisonOperator()) {
221 48
            $comparisonToken = clone $this->_getCurrentToken();
222 48
            $this->_lexer->nextToken();
223 48
            $right = $this->_parseAdditive();
224 48
            $left = self::_generateComparisonExpression(
225 48
                $left, $right,
226 48
                $comparisonToken, $this->_isPHPExpressionProvider
0 ignored issues
show
Bug introduced by
$this->_isPHPExpressionProvider of type boolean is incompatible with the type POData\Providers\Metadata\Type\Boolean expected by parameter $isPHPExpressionProvider of POData\UriProcessor\Quer...eComparisonExpression(). ( Ignorable by Annotation )

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

226
                $comparisonToken, /** @scrutinizer ignore-type */ $this->_isPHPExpressionProvider
Loading history...
227 48
            );
228
        }
229
230 57
        $this->_recurseLeave();
231 57
        return $left;
232
    }
233
234
    /**
235
     * Parse additive operation (add, sub).
236
     *
237
     * @return AbstractExpression
238
     */
239 57
    private function _parseAdditive()
240
    {
241 57
        $this->_recurseEnter();
242 57
        $left = $this->_parseMultiplicative();
243 57
        while ($this->_getCurrentToken()->identifierIs(ODataConstants::KEYWORD_ADD)
0 ignored issues
show
Bug introduced by
POData\Common\ODataConstants::KEYWORD_ADD of type string is incompatible with the type POData\UriProcessor\Quer...arser\ExpressionTokenId expected by parameter $id of POData\UriProcessor\Quer...onToken::identifierIs(). ( Ignorable by Annotation )

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

243
        while ($this->_getCurrentToken()->identifierIs(/** @scrutinizer ignore-type */ ODataConstants::KEYWORD_ADD)
Loading history...
244 57
            || $this->_getCurrentToken()->identifierIs(ODataConstants::KEYWORD_SUB)) {
245 7
            $additiveToken = clone $this->_getCurrentToken();
246 7
            $this->_lexer->nextToken();
247 7
            $right = $this->_parseMultiplicative();
248 7
            $opReturnType = FunctionDescription::verifyAndPromoteArithmeticOpArguments($additiveToken, $left, $right);
249 7
            if ($additiveToken->identifierIs(ODataConstants::KEYWORD_ADD)) {
250 7
                $left = new ArithmeticExpression($left, $right, ExpressionType::ADD, $opReturnType);
0 ignored issues
show
Bug introduced by
POData\UriProcessor\Quer...ons\ExpressionType::ADD of type integer is incompatible with the type POData\UriProcessor\Quer...ressions\ExpressionType expected by parameter $nodeType of POData\UriProcessor\Quer...pression::__construct(). ( Ignorable by Annotation )

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

250
                $left = new ArithmeticExpression($left, $right, /** @scrutinizer ignore-type */ ExpressionType::ADD, $opReturnType);
Loading history...
251
            } else {
252 2
                $left = new ArithmeticExpression($left, $right, ExpressionType::SUBTRACT, $opReturnType);
253
            }
254
        }
255
256 57
        $this->_recurseLeave();
257 57
        return $left;
258
    }
259
260
    /**
261
     * Parse multipicative operators (mul, div, mod)
262
     *
263
     * @return AbstractExpression
264
     */
265 57
    private function  _parseMultiplicative()
266
    {
267 57
        $this->_recurseEnter();
268 57
        $left = $this->_parseUnary();
269 57
        while ($this->_getCurrentToken()->identifierIs(ODataConstants::KEYWORD_MULTIPLY)
0 ignored issues
show
Bug introduced by
POData\Common\ODataConstants::KEYWORD_MULTIPLY of type string is incompatible with the type POData\UriProcessor\Quer...arser\ExpressionTokenId expected by parameter $id of POData\UriProcessor\Quer...onToken::identifierIs(). ( Ignorable by Annotation )

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

269
        while ($this->_getCurrentToken()->identifierIs(/** @scrutinizer ignore-type */ ODataConstants::KEYWORD_MULTIPLY)
Loading history...
270 57
            || $this->_getCurrentToken()->identifierIs(ODataConstants::KEYWORD_DIVIDE)
271 57
            || $this->_getCurrentToken()->identifierIs(ODataConstants::KEYWORD_MODULO)
272
        ) {
273 5
            $multiplicativeToken = clone $this->_getCurrentToken();
274 5
            $this->_lexer->nextToken();
275 5
            $right = $this->_parseUnary();
276 5
            $opReturnType = FunctionDescription::verifyAndPromoteArithmeticOpArguments(
277 5
                    $multiplicativeToken, $left, $right
278 5
                );
279 5
            if ($multiplicativeToken->identifierIs(ODataConstants::KEYWORD_MULTIPLY)) {
280 3
                $left = new ArithmeticExpression($left, $right, ExpressionType::MULTIPLY, $opReturnType);
0 ignored issues
show
Bug introduced by
POData\UriProcessor\Quer...xpressionType::MULTIPLY of type integer is incompatible with the type POData\UriProcessor\Quer...ressions\ExpressionType expected by parameter $nodeType of POData\UriProcessor\Quer...pression::__construct(). ( Ignorable by Annotation )

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

280
                $left = new ArithmeticExpression($left, $right, /** @scrutinizer ignore-type */ ExpressionType::MULTIPLY, $opReturnType);
Loading history...
281 2
            } else if ($multiplicativeToken->identifierIs(ODataConstants::KEYWORD_DIVIDE)) {
282
                $left = new ArithmeticExpression($left, $right, ExpressionType::DIVIDE, $opReturnType);
283
            } else {
284 2
                $left = new ArithmeticExpression($left, $right, ExpressionType::MODULO, $opReturnType);
285
            }
286
        }
287
288 57
        $this->_recurseLeave();
289 57
        return $left;
290
    }
291
292
    /**
293
     * Parse unary operator (- ,not)
294
     *
295
     * @return AbstractExpression
296
     */
297 57
    private function _parseUnary()
298
    {
299 57
        $this->_recurseEnter();
300
301 57
        if ($this->_getCurrentToken()->Id == ExpressionTokenId::MINUS
302 57
            || $this->_getCurrentToken()->identifierIs(ODataConstants::KEYWORD_NOT)
0 ignored issues
show
Bug introduced by
POData\Common\ODataConstants::KEYWORD_NOT of type string is incompatible with the type POData\UriProcessor\Quer...arser\ExpressionTokenId expected by parameter $id of POData\UriProcessor\Quer...onToken::identifierIs(). ( Ignorable by Annotation )

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

302
            || $this->_getCurrentToken()->identifierIs(/** @scrutinizer ignore-type */ ODataConstants::KEYWORD_NOT)
Loading history...
303
        ) {
304 4
            $op = clone $this->_getCurrentToken();
305 4
            $this->_lexer->nextToken();
306 4
            if ($op->Id == ExpressionTokenId::MINUS
0 ignored issues
show
introduced by
The condition $op->Id == POData\UriPro...xpressionTokenId::MINUS is always false.
Loading history...
307 4
                && (ExpressionLexer::isNumeric($this->_getCurrentToken()->Id))
0 ignored issues
show
Bug introduced by
$this->_getCurrentToken()->Id of type void is incompatible with the type POData\UriProcessor\Quer...arser\ExpressionTokenId expected by parameter $id of POData\UriProcessor\Quer...ssionLexer::isNumeric(). ( Ignorable by Annotation )

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

307
                && (ExpressionLexer::isNumeric(/** @scrutinizer ignore-type */ $this->_getCurrentToken()->Id))
Loading history...
308
            ) {
309
                $numberLiteral = $this->_getCurrentToken();
310
                $numberLiteral->Text = '-' . $numberLiteral->Text;
311
                $numberLiteral->Position = $op->Position;
312
                $v = $this->_getCurrentToken();
0 ignored issues
show
Unused Code introduced by
The assignment to $v is dead and can be removed.
Loading history...
313
                $this->_setCurrentToken($numberLiteral);
314
                $this->_recurseLeave();
315
                return $this->_parsePrimary();
316
            }
317
318 4
            $expr = $this->_parsePrimary();
319 4
            FunctionDescription::validateUnaryOpArguments($op, $expr);
320 4
            if ($op->Id == ExpressionTokenId::MINUS) {
0 ignored issues
show
introduced by
The condition $op->Id == POData\UriPro...xpressionTokenId::MINUS is always false.
Loading history...
321 3
                $expr = new UnaryExpression($expr, ExpressionType::NEGATE, $expr->getType());
322
            } else {
323 2
                $expr = new UnaryExpression($expr, ExpressionType::NOT_LOGICAL, new Boolean());
0 ignored issues
show
Bug introduced by
POData\UriProcessor\Quer...essionType::NOT_LOGICAL of type integer is incompatible with the type POData\UriProcessor\Quer...xpressions\unknown_type expected by parameter $nodeType of POData\UriProcessor\Quer...pression::__construct(). ( Ignorable by Annotation )

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

323
                $expr = new UnaryExpression($expr, /** @scrutinizer ignore-type */ ExpressionType::NOT_LOGICAL, new Boolean());
Loading history...
324
            }
325
326 4
            $this->_recurseLeave();
327 4
            return $expr;
328
        }
329
330 57
        $this->_recurseLeave();
331 57
        return $this->_parsePrimary();
332
    }
333
334
    /**
335
     * Start parsing the primary.
336
     *
337
     * @return AbstractExpression
338
     */
339 57
    private function _parsePrimary()
340
    {
341 57
        $this->_recurseEnter();
342 57
        $expr = $this->_parsePrimaryStart();
343 57
        while (true) {
344 57
            if ($this->_getCurrentToken()->Id == ExpressionTokenId::SLASH) {
345 5
                $this->_lexer->nextToken();
346 5
                $expr = $this->_parsePropertyAccess($expr);
347
            } else {
348 57
                break;
349
            }
350
        }
351
352 57
        $this->_recurseLeave();
353 57
        return $expr;
354
    }
355
356
    /**
357
     * Parse primary tokens [literals, identifiers (e.g. function call), open param for sub expressions]
358
     *
359
     *
360
     * @return AbstractExpression
361
     */
362 57
    private function _parsePrimaryStart()
363
    {
364 57
        switch ($this->_lexer->getCurrentToken()->Id) {
365
            case ExpressionTokenId::BOOLEAN_LITERAL:
366 5
                return $this->_parseTypedLiteral(new Boolean());
367
            case ExpressionTokenId::DATETIME_LITERAL:
368 8
                return $this->_parseTypedLiteral(new DateTime());
369
            case ExpressionTokenId::DECIMAL_LITERAL:
370 3
                return $this->_parseTypedLiteral(new Decimal());
371
            case ExpressionTokenId::NULL_LITERAL:
372 7
                return $this->_parseNullLiteral();
373
            case ExpressionTokenId::IDENTIFIER:
374 55
                return $this->_parseIdentifier();
375
            case ExpressionTokenId::STRING_LITERAL:
376 33
                return $this->_parseTypedLiteral(new StringType());
377
            case ExpressionTokenId::INT64_LITERAL:
378 1
                return $this->_parseTypedLiteral(new Int64());
379
            case ExpressionTokenId::INTEGER_LITERAL:
380 30
                return $this->_parseTypedLiteral(new Int32());
381
            case ExpressionTokenId::DOUBLE_LITERAL:
382 7
                return $this->_parseTypedLiteral(new Double());
383
            case ExpressionTokenId::SINGLE_LITERAL:
384 4
                return $this->_parseTypedLiteral(new Single());
385
            case ExpressionTokenId::GUID_LITERAL:
386 1
                return $this->_parseTypedLiteral(new Guid());
387
            case ExpressionTokenId::ARRAY_LITERAL:
388
                return $this->_parseTypedLiteral(new ArrayType());
389
            case ExpressionTokenId::BINARY_LITERAL:
390
                throw new NotImplementedException(
391
                    'Support for binary is not implemented'
392
                );
393
                //return $this->_parseTypedLiteral(new Binary());
394
            case ExpressionTokenId::OPENPARAM:
395 5
                return $this->_parseParenExpression();
396
            default:
397
                throw ODataException::createSyntaxError("Expression expected.");
398
        }
399
    }
400
401
    /**
402
     * Parse Sub expression.
403
     *
404
     * @return AbstractExpression
405
     */
406 5
    private function _parseParenExpression()
407
    {
408 5
        if ($this->_getCurrentToken()->Id != ExpressionTokenId::OPENPARAM) {
0 ignored issues
show
introduced by
The condition $this->_getCurrentToken(...ssionTokenId::OPENPARAM is always true.
Loading history...
409
            throw ODataException::createSyntaxError("Open parenthesis expected.");
410
        }
411
412 5
        $this->_lexer->nextToken();
413 5
        $expr = $this->_parseExpression();
414 5
        if ($this->_getCurrentToken()->Id == ExpressionTokenId::COMMA) {
415
            $expr = [$expr];
416
            while ($this->_getCurrentToken()->Id == ExpressionTokenId::COMMA)
417
            {
418
                $this->_lexer->nextToken();
419
                $expr[] = $this->_parseExpression();
420
            }
421
        }
422 5
        if ($this->_getCurrentToken()->Id != ExpressionTokenId::CLOSEPARAM) {
423
            throw ODataException::createSyntaxError("Close parenthesis expected.");
424
        }
425
426 5
        $this->_lexer->nextToken();
427 5
        return $expr;
428
    }
429
430
    /**
431
     * Parse an identifier
432
     *
433
     * @return AbstractExpression
434
     */
435 55
    private function _parseIdentifier()
436
    {
437 55
        $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 $expressionTokenId of POData\UriProcessor\Quer...arser::_validateToken(). ( Ignorable by Annotation )

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

437
        $this->_validateToken(/** @scrutinizer ignore-type */ ExpressionTokenId::IDENTIFIER);
Loading history...
438
439
        // An open paren here would indicate calling a method
440 55
        $identifierIsFunction = $this->_lexer->peekNextToken()->Id == ExpressionTokenId::OPENPARAM;
441 55
        if ($identifierIsFunction) {
0 ignored issues
show
introduced by
The condition $identifierIsFunction is always false.
Loading history...
442 35
            return $this->_parseIdentifierAsFunction();
443
        } else {
444 54
            return $this->_parsePropertyAccess(null);
445
        }
446
    }
447
448
    /**
449
     * Parse a property access
450
     *
451
     * @param PropertyAccessExpression $parentExpression Parent expression.
452
     *
453
     * @throws ODataException
454
     *
455
     * @return PropertyAccessExpression
456
     */
457 54
    private function _parsePropertyAccess($parentExpression)
458
    {
459 54
        $identifier = $this->_getCurrentToken()->getIdentifier();
460 54
        if (is_null($parentExpression)) {
461 54
            $parentResourceType = $this->_resourceType;
462
        } else {
463 5
            $parentResourceType = $parentExpression->getResourceType();
464 5
            $this->_hasLevel2PropertyInTheExpression = true;
465
        }
466
467 54
        $resourceProperty = $parentResourceType->resolveProperty($identifier);
468 54
        if (is_null($resourceProperty)) {
469 1
            throw ODataException::createSyntaxError(
470 1
                Messages::expressionLexerNoPropertyInType(
471 1
                    $identifier,
472 1
                    $parentResourceType->getFullName(),
473 1
                    $this->_getCurrentToken()->Position
474 1
                )
475 1
            );
476
        }
477
478
        /*
479
        if ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCESET_REFERENCE) {
480
            throw ODataException::createSyntaxError(
481
                Messages::expressionParserEntityCollectionNotAllowedInFilter(
482
                    $resourceProperty->getName(),
483
                    $parentResourceType->getFullName(),
484
                    $this->_getCurrentToken()->Position
485
                )
486
            );
487
        }
488
        */
489
490 54
        $exp = new PropertyAccessExpression($parentExpression, $resourceProperty);
491 54
        $this->_lexer->nextToken();
492 54
        return $exp;
493
    }
494
495
    /**
496
     * Try to parse an identifier which is followed by an opern bracket as
497
     * astoria URI function call.
498
     *
499
     * @return AbstractExpression
500
     *
501
     * @throws ODataException
502
     */
503 35
    private function _parseIdentifierAsFunction()
504
    {
505 35
        $functionToken = clone $this->_getCurrentToken();
506 35
        $functions = FunctionDescription::verifyFunctionExists($functionToken);
507 35
        $this->_lexer->nextToken();
508 35
        $paramExpressions = $this->_parseArgumentList();
509 35
        $function = FunctionDescription::verifyFunctionCallOpArguments(
510 35
            $functions, $paramExpressions, $functionToken
511 35
        );
512 35
        return new FunctionCallExpression($function, $paramExpressions);
513
    }
514
515
    /**
516
     * Start parsing argument list of a function-call
517
     *
518
     * @return array<AbstractExpression>
519
     */
520 35
    private function _parseArgumentList()
521
    {
522 35
        if ($this->_getCurrentToken()->Id != ExpressionTokenId::OPENPARAM) {
0 ignored issues
show
introduced by
The condition $this->_getCurrentToken(...ssionTokenId::OPENPARAM is always true.
Loading history...
523
            throw ODataException::createSyntaxError("Open parenthesis expected.");
524
        }
525
526 35
        $this->_lexer->nextToken();
527 35
        $args
528 35
            = $this->_getCurrentToken()->Id != ExpressionTokenId::CLOSEPARAM
529 35
             ? $this->_parseArguments() : array();
530 35
        if ($this->_getCurrentToken()->Id != ExpressionTokenId::CLOSEPARAM) {
531 1
            throw ODataException::createSyntaxError("Close parenthesis expected.");
532
        }
533
534 35
        $this->_lexer->nextToken();
535 35
        return $args;
536
    }
537
538
    /**
539
     * Parse arguments of  a function-call.
540
     *
541
     * @return array<AbstractExpression>
542
     */
543 35
    private function _parseArguments()
544
    {
545 35
        $argList = array();
546 35
        while (true) {
547 35
            $argList[] = $this->_parseExpression();
548 35
            if ($this->_getCurrentToken()->Id != ExpressionTokenId::COMMA) {
549 35
                break;
550
            }
551
552 23
            $this->_lexer->nextToken();
553
        }
554
555 35
        return $argList;
556
    }
557
558
    /**
559
     * Parse primitive type literal.
560
     *
561
     * @param IType $targetType Expected type of the current literal.
562
     *
563
     * @return AbstractExpression
564
     *
565
     * @throws ODataException
566
     */
567 53
    private function _parseTypedLiteral(IType $targetType)
568
    {
569 53
        $literal = $this->_lexer->getCurrentToken()->Text;
570 53
        $outVal = null;
571 53
        if (!$targetType->validate($literal, $outVal)) {
572 1
            throw ODataException::createSyntaxError(
573 1
                Messages::expressionParserUnrecognizedLiteral(
574 1
                    $targetType->getFullTypeName(),
575 1
                    $literal,
576 1
                    $this->_lexer->getCurrentToken()->Position
577 1
                )
578 1
            );
579
        }
580
581 53
        $result = new ConstantExpression($outVal, $targetType);
582 53
        $this->_lexer->nextToken();
583 53
        return $result;
584
    }
585
586
    /**
587
     * Parse null literal.
588
     *
589
     * @return ConstantExpression
590
     */
591 7
    private function _parseNullLiteral()
592
    {
593 7
        $this->_lexer->nextToken();
594 7
        return new ConstantExpression(null, new NullType());
595
    }
596
597
    /**
598
     * Check the current token is of a specific kind
599
     *
600
     * @param ExpressionTokenId $expressionTokenId Token to check
601
     *                                             with current token.
602
     *
603
     * @return boolean
604
     */
605 57
    private function _tokenIdentifierIs($expressionTokenId)
606
    {
607 57
        return $this->_getCurrentToken()->identifierIs($expressionTokenId);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_getCurren...rIs($expressionTokenId) returns the type boolean which is incompatible with the documented return type POData\Providers\Metadata\Type\Boolean.
Loading history...
608
    }
609
610
    /**
611
     * Validate the current token
612
     *
613
     * @param ExpressionTokenId $expressionTokenId Token to check
614
     *                                             with current token.
615
     *
616
     * @return void
617
     *
618
     * @throws ODataException
619
     */
620 55
    private function _validateToken($expressionTokenId)
621
    {
622 55
        if ($this->_getCurrentToken()->Id != $expressionTokenId) {
623
            throw ODataException::createSyntaxError("Syntax error.");
624
        }
625
    }
626
627
    /**
628
     * Increment recursion count and throw error if beyond limit
629
     *
630
     * @return void
631
     *
632
     * @throws ODataException If max recursion limit hits.
633
     */
634 57
    private function _recurseEnter()
635
    {
636 57
        $this->_recursionDepth++;
637 57
        if ($this->_recursionDepth == self::RECURSION_LIMIT) {
638
            throw ODataException::createSyntaxError("Recursion limit reached.");
639
        }
640
    }
641
642
    /**
643
     * Decrement recursion count
644
     *
645
     * @return void
646
     */
647 57
    private function _recurseLeave()
648
    {
649 57
        $this->_recursionDepth--;
650
    }
651
652
    /**
653
     * Generates Comparison Expression
654
     *
655
     * @param AbstractExpression $left                       The LHS expression.
656
     * @param AbstractExpression $right                      The RHS expression.
657
     * @param ExpressionToken    $expressionToken            The comparison expression token.
658
     * @param boolean            $isPHPExpressionProvider
659
     *
660
     * @return AbstractExpression
661
     */
662 48
    private static function _generateComparisonExpression($left, $right, $expressionToken, $isPHPExpressionProvider)
663
    {
664 48
        FunctionDescription::verifyRelationalOpArguments($expressionToken, $left, $right);
665
666
        //We need special handling for comparison of following types:
667
        //1. EdmString
668
        //2. DateTime
669
        //3. Guid
670
        //4. Binary
671
        //Will make these comparison as function calls, which will
672
        // be converted to language specific function call by expression
673
        // provider
674 48
        $string = new StringType();
675 48
        if ($left->typeIs($string) && $right->typeIs($string) && !in_array($expressionToken->Text, [ODataConstants::KEYWORD_EQUAL, ODataConstants::KEYWORD_NOT_EQUAL])) {
676 3
            $strcmpFunctions = FunctionDescription::stringComparisonFunctions();
677 3
            $left = new FunctionCallExpression($strcmpFunctions[0], array($left, $right));
678 3
            $right = new ConstantExpression(0, new Int32());
679
        }
680
681
        // $dateTime = new DateTime();
682
        // if ($left->typeIs($dateTime) && $right->typeIs($dateTime)) {
683
        //     $dateTimeCmpFunctions = FunctionDescription::dateTimeComparisonFunctions();
684
        //     $left = new FunctionCallExpression($dateTimeCmpFunctions[0], array($left, $right));
685
        //     $right = new ConstantExpression(0, new Int32());
686
        // }
687
688 48
        $guid = new Guid();
689 48
        if ($left->typeIs($guid) && $right->typeIs($guid)) {
690 1
            $guidEqualityFunctions = FunctionDescription::guidEqualityFunctions();
691 1
            $left = new FunctionCallExpression($guidEqualityFunctions[0], array($left, $right));
692 1
            $right = new ConstantExpression(true, new Boolean());
693
        }
694
695 48
        $binary = new Binary();
696 48
        if ($left->typeIs($binary) && $right->typeIs($binary)) {
697
            $binaryEqualityFunctions = FunctionDescription::binaryEqualityFunctions();
698
            $left = new FunctionCallExpression($binaryEqualityFunctions[0], array($left, $right));
699
            $right = new ConstantExpression(true, new Boolean());
700
        }
701
702 48
        $null = new NullType();
703 48
        if ($left->typeIs($null) || $right->typeIs($null)) {
704
            // If the end user is responsible for implementing IExpressionProvider
705
            // then the sub-tree for a nullability check would be:
706
            //
707
            //          RelationalExpression(EQ/NE)
708
            //                    |
709
            //               ------------
710
            //               |           |
711
            //               |           |
712
            //            CustomerID    NULL
713
            //
714
            // Otherwise (In case of default PHPExpressionProvider):
715
            //
716
            //  CustomerID eq null
717
            //  ==================
718
            //
719
            //              FunctionCallExpression(is_null)
720
            //                       |
721
            //                       |- Signature => bool (typeof(CustomerID))
722
            //                       |- args => {CustomerID}
723
            //
724
            //
725
            //  CustomerID ne null
726
            //  ==================
727
            //
728
            //              UnaryExpression (not)
729
            //                       |
730
            //              FunctionCallExpression(is_null)
731
            //                       |
732
            //                       |- Signature => bool (typeof(CustomerID))
733
            //                       |- args => {CustomerID}
734
            //
735 7
            if ($isPHPExpressionProvider) {
0 ignored issues
show
introduced by
$isPHPExpressionProvider is of type POData\Providers\Metadata\Type\Boolean, thus it always evaluated to true.
Loading history...
736 4
                $arg = $left->typeIs($null) ? $right : $left;
737 4
                $isNullFunctionDescription = new FunctionDescription('is_null', new Boolean(), array($arg->getType()));
738 4
                switch ($expressionToken->Text) {
739
                    case ODataConstants::KEYWORD_EQUAL:
740 4
                        return new FunctionCallExpression($isNullFunctionDescription, array($arg));
741
                        break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
742
743
                    case ODataConstants::KEYWORD_NOT_EQUAL:
744
                        return new UnaryExpression(
745
                            new FunctionCallExpression($isNullFunctionDescription, array($arg)),
746
                            ExpressionType::NOT_LOGICAL,
0 ignored issues
show
Bug introduced by
POData\UriProcessor\Quer...essionType::NOT_LOGICAL of type integer is incompatible with the type POData\UriProcessor\Quer...xpressions\unknown_type expected by parameter $nodeType of POData\UriProcessor\Quer...pression::__construct(). ( Ignorable by Annotation )

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

746
                            /** @scrutinizer ignore-type */ ExpressionType::NOT_LOGICAL,
Loading history...
747
                            new Boolean()
748
                        );
749
                        break;
750
                }
751
            }
752
        }
753
754 48
        switch ($expressionToken->Text) {
755
            case ODataConstants::KEYWORD_EQUAL:
756 44
                return new RelationalExpression(
757 44
                    $left, $right, ExpressionType::EQUAL
0 ignored issues
show
Bug introduced by
POData\UriProcessor\Quer...s\ExpressionType::EQUAL of type integer is incompatible with the type POData\UriProcessor\Quer...ressions\ExpressionType expected by parameter $nodeType of POData\UriProcessor\Quer...pression::__construct(). ( Ignorable by Annotation )

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

757
                    $left, $right, /** @scrutinizer ignore-type */ ExpressionType::EQUAL
Loading history...
758 44
                );
759
            case ODataConstants::KEYWORD_NOT_EQUAL:
760 2
                return new RelationalExpression(
761 2
                    $left, $right, ExpressionType::NOTEQUAL
762 2
                );
763
            case ODataConstants::KEYWORD_GREATERTHAN:
764 6
                return new RelationalExpression(
765 6
                    $left, $right, ExpressionType::GREATERTHAN
766 6
                );
767
            case ODataConstants::KEYWORD_GREATERTHAN_OR_EQUAL:
768 3
                return new RelationalExpression(
769 3
                    $left, $right, ExpressionType::GREATERTHAN_OR_EQUAL
770 3
                );
771
            case ODataConstants::KEYWORD_LESSTHAN:
772
                return new RelationalExpression(
773
                    $left, $right, ExpressionType::LESSTHAN
774
                );
775
            case ODataConstants::KEYWORD_IN:
776
                return new RelationalExpression(
777
                    $left, $right, ExpressionType::IN
778
                );
779
            default:
780 3
                return new RelationalExpression(
781 3
                    $left, $right, ExpressionType::LESSTHAN_OR_EQUAL
782 3
                );
783
        }
784
    }
785
786
}
787