Passed
Pull Request — master (#120)
by Alex
03:44
created

ExpressionParser::generateComparisonExpression()   F

Complexity

Conditions 20
Paths 416

Size

Total Lines 130
Code Lines 68

Duplication

Lines 20
Ratio 15.38 %

Importance

Changes 0
Metric Value
dl 20
loc 130
rs 3.5538
c 0
b 0
f 0
cc 20
eloc 68
nc 416
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace POData\UriProcessor\QueryProcessor\ExpressionParser;
4
5
use POData\Common\Messages;
6
use POData\Common\NotImplementedException;
7
use POData\Common\ODataConstants;
8
use POData\Common\ODataException;
9
use POData\Providers\Metadata\ResourcePropertyKind;
10
use POData\Providers\Metadata\ResourceType;
11
use POData\Providers\Metadata\Type\Binary;
12
use POData\Providers\Metadata\Type\Boolean;
13
use POData\Providers\Metadata\Type\DateTime;
14
use POData\Providers\Metadata\Type\Decimal;
15
use POData\Providers\Metadata\Type\Double;
16
use POData\Providers\Metadata\Type\Guid;
17
use POData\Providers\Metadata\Type\Int32;
18
use POData\Providers\Metadata\Type\Int64;
19
use POData\Providers\Metadata\Type\IType;
20
use POData\Providers\Metadata\Type\Null1;
21
use POData\Providers\Metadata\Type\Single;
22
use POData\Providers\Metadata\Type\StringType;
23
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\AbstractExpression;
24
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\ArithmeticExpression;
25
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\ConstantExpression;
26
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\ExpressionType;
27
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\FunctionCallExpression;
28
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\LogicalExpression;
29
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\PropertyAccessExpression;
30
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\RelationalExpression;
31
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\UnaryExpression;
32
use POData\UriProcessor\QueryProcessor\FunctionDescription;
33
34
/**
35
 * Class ExpressionParser.
36
 */
37
class ExpressionParser
38
{
39
    const RECURSION_LIMIT = 200;
40
41
    /**
42
     * The Lexical analyzer.
43
     *
44
     * @var ExpressionLexer
45
     */
46
    private $lexer;
47
48
    /**
49
     * The current recursion depth.
50
     *
51
     * @var int
52
     */
53
    private $recursionDepth;
54
55
    /**
56
     * The ResourceType on which $filter condition needs to be applied.
57
     *
58
     * @var ResourceType
59
     */
60
    private $resourceType;
61
62
    /**
63
     * @var bool
64
     */
65
    private $isPHPExpressionProvider;
66
67
    /**
68
     * True if the filter expression contains level 2 property access, for example
69
     * Customers?$filter=Address/LineNumber eq 12
70
     * Customer?$filter=Order/OrderID gt 1234
71
     * False otherwise.
72
     *
73
     * @var bool
74
     */
75
    private $hasLevel2PropertyInTheExpression;
76
77
    /**
78
     * Construct a new instance of ExpressionParser.
79
     *
80
     * @param string       $text                    The expression to parse
81
     * @param ResourceType $resourceType            The resource type of the resource targeted by the resource path
82
     * @param bool         $isPHPExpressionProvider
83
     *
84
     * TODO Expression parser should not depend on the fact that end user is implementing IExpressionProvider or not
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
85
     */
86
    public function __construct($text, ResourceType $resourceType, $isPHPExpressionProvider)
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $isPHPExpressionProvider exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
87
    {
88
        $this->lexer = new ExpressionLexer($text);
89
        $this->resourceType = $resourceType;
90
        $this->isPHPExpressionProvider = $isPHPExpressionProvider;
91
        $this->hasLevel2PropertyInTheExpression = false;
92
    }
93
94
    /**
95
     * Checks whether the expression contains level 2 property access.
96
     *
97
     * @return bool
98
     */
99
    public function hasLevel2Property()
100
    {
101
        return $this->hasLevel2PropertyInTheExpression;
102
    }
103
104
    /**
105
     * Get the current token from lexer.
106
     *
107
     * @return ExpressionToken
108
     */
109
    private function getCurrentToken()
110
    {
111
        return $this->lexer->getCurrentToken();
112
    }
113
114
    /**
115
     * Set the current token in lexer.
116
     *
117
     * @param ExpressionToken $token The token to set as current token
118
     */
119
    private function setCurrentToken($token)
120
    {
121
        $this->lexer->setCurrentToken($token);
122
    }
123
124
    /**
125
     * Resets parser with new expression string.
126
     *
127
     * @param string $text Reset the expression to parse
128
     */
129
    public function resetParser($text)
130
    {
131
        $this->lexer = new ExpressionLexer($text);
132
        $this->recursionDepth = 0;
133
    }
134
135
    /**
136
     * Parse the expression in filter option.
137
     *
138
     * @return AbstractExpression
139
     */
140
    public function parseFilter()
141
    {
142
        return $this->parseExpression();
143
    }
144
145
    /**
146
     * Start parsing the expression.
147
     *
148
     * @return AbstractExpression
149
     */
150
    private function parseExpression()
151
    {
152
        $this->recurseEnter();
153
        $expr = $this->parseLogicalOr();
154
        $this->recurseLeave();
155
156
        return $expr;
157
    }
158
159
    /**
160
     * Parse logical or (or).
161
     *
162
     * @return AbstractExpression
163
     */
164 View Code Duplication
    private function parseLogicalOr()
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...
165
    {
166
        $this->recurseEnter();
167
        $left = $this->parseLogicalAnd();
168
        while ($this->tokenIdentifierIs(ODataConstants::KEYWORD_OR)) {
169
            $logicalOpToken = clone $this->getCurrentToken();
170
            $this->lexer->nextToken();
171
            $right = $this->parseLogicalAnd();
172
            FunctionDescription::verifyLogicalOpArguments($logicalOpToken, $left, $right);
173
            $left = new LogicalExpression(
174
                $left,
175
                $right,
176
                ExpressionType::OR_LOGICAL
177
            );
178
        }
179
180
        $this->recurseLeave();
181
182
        return $left;
183
    }
184
185
    /**
186
     * Parse logical and (and).
187
     *
188
     * @return AbstractExpression
189
     */
190 View Code Duplication
    private function parseLogicalAnd()
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...
191
    {
192
        $this->recurseEnter();
193
        $left = $this->parseComparison();
194
        while ($this->tokenIdentifierIs(ODataConstants::KEYWORD_AND)) {
195
            $logicalOpToken = clone $this->getCurrentToken();
196
            $this->lexer->nextToken();
197
            $right = $this->parseComparison();
198
            FunctionDescription::verifyLogicalOpArguments($logicalOpToken, $left, $right);
199
            $left = new LogicalExpression($left, $right, ExpressionType::AND_LOGICAL);
200
        }
201
202
        $this->recurseLeave();
203
204
        return $left;
205
    }
206
207
    /**
208
     * Parse comparison operation (eq, ne, gt, ge, lt, le).
209
     *
210
     * @return AbstractExpression
211
     */
212
    private function parseComparison()
213
    {
214
        $this->recurseEnter();
215
        $left = $this->parseAdditive();
216
        while ($this->getCurrentToken()->isComparisonOperator()) {
217
            $comparisonToken = clone $this->getCurrentToken();
218
            $this->lexer->nextToken();
219
            $right = $this->parseAdditive();
220
            $left = self::generateComparisonExpression(
221
                $left,
222
                $right,
223
                $comparisonToken,
224
                $this->isPHPExpressionProvider
225
            );
226
        }
227
228
        $this->recurseLeave();
229
230
        return $left;
231
    }
232
233
    /**
234
     * Parse additive operation (add, sub).
235
     *
236
     * @return AbstractExpression
237
     */
238
    private function parseAdditive()
239
    {
240
        $this->recurseEnter();
241
        $left = $this->parseMultiplicative();
242
        while ($this->getCurrentToken()->identifierIs(ODataConstants::KEYWORD_ADD)
243
            || $this->getCurrentToken()->identifierIs(ODataConstants::KEYWORD_SUB)) {
244
            $additiveToken = clone $this->getCurrentToken();
245
            $this->lexer->nextToken();
246
            $right = $this->parseMultiplicative();
247
            $opReturnType = FunctionDescription::verifyAndPromoteArithmeticOpArguments($additiveToken, $left, $right);
248
            if ($additiveToken->identifierIs(ODataConstants::KEYWORD_ADD)) {
249
                $left = new ArithmeticExpression($left, $right, ExpressionType::ADD, $opReturnType);
250
            } else {
251
                $left = new ArithmeticExpression($left, $right, ExpressionType::SUBTRACT, $opReturnType);
252
            }
253
        }
254
255
        $this->recurseLeave();
256
257
        return $left;
258
    }
259
260
    /**
261
     * Parse multipicative operators (mul, div, mod).
262
     *
263
     * @return AbstractExpression
264
     */
265
    private function parseMultiplicative()
266
    {
267
        $this->recurseEnter();
268
        $left = $this->parseUnary();
269
        while ($this->getCurrentToken()->identifierIs(ODataConstants::KEYWORD_MULTIPLY)
270
            || $this->getCurrentToken()->identifierIs(ODataConstants::KEYWORD_DIVIDE)
271
            || $this->getCurrentToken()->identifierIs(ODataConstants::KEYWORD_MODULO)
272
        ) {
273
            $multiplicativeToken = clone $this->getCurrentToken();
274
            $this->lexer->nextToken();
275
            $right = $this->parseUnary();
276
            $opReturnType = FunctionDescription::verifyAndPromoteArithmeticOpArguments(
277
                $multiplicativeToken,
278
                $left,
279
                $right
280
            );
281
            if ($multiplicativeToken->identifierIs(ODataConstants::KEYWORD_MULTIPLY)) {
282
                $left = new ArithmeticExpression($left, $right, ExpressionType::MULTIPLY, $opReturnType);
283
            } elseif ($multiplicativeToken->identifierIs(ODataConstants::KEYWORD_DIVIDE)) {
284
                $left = new ArithmeticExpression($left, $right, ExpressionType::DIVIDE, $opReturnType);
285
            } else {
286
                $left = new ArithmeticExpression($left, $right, ExpressionType::MODULO, $opReturnType);
287
            }
288
        }
289
290
        $this->recurseLeave();
291
292
        return $left;
293
    }
294
295
    /**
296
     * Parse unary operator (- ,not).
297
     *
298
     * @return AbstractExpression
299
     */
300
    private function parseUnary()
301
    {
302
        $this->recurseEnter();
303
304
        if ($this->getCurrentToken()->Id == ExpressionTokenId::MINUS
305
            || $this->getCurrentToken()->identifierIs(ODataConstants::KEYWORD_NOT)
306
        ) {
307
            $op = clone $this->getCurrentToken();
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $op. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
308
            $this->lexer->nextToken();
309
            if ($op->Id == ExpressionTokenId::MINUS
310
                && (ExpressionLexer::isNumeric($this->getCurrentToken()->Id))
311
            ) {
312
                $numberLiteral = $this->getCurrentToken();
313
                $numberLiteral->Text = '-' . $numberLiteral->Text;
314
                $numberLiteral->Position = $op->Position;
315
                $v = $this->getCurrentToken();
0 ignored issues
show
Unused Code introduced by
$v is not used, you could remove the assignment.

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

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

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

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

Loading history...
Comprehensibility introduced by
Avoid variables with short names like $v. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
316
                $this->setCurrentToken($numberLiteral);
317
                $this->recurseLeave();
318
319
                return $this->parsePrimary();
320
            }
321
322
            $expr = $this->parsePrimary();
323
            FunctionDescription::validateUnaryOpArguments($op, $expr);
324
            if ($op->Id == ExpressionTokenId::MINUS) {
325
                $expr = new UnaryExpression($expr, ExpressionType::NEGATE, $expr->getType());
326
            } else {
327
                $expr = new UnaryExpression($expr, ExpressionType::NOT_LOGICAL, new Boolean());
328
            }
329
330
            $this->recurseLeave();
331
332
            return $expr;
333
        }
334
335
        $this->recurseLeave();
336
337
        return $this->parsePrimary();
338
    }
339
340
    /**
341
     * Start parsing the primary.
342
     *
343
     * @return AbstractExpression
344
     */
345
    private function parsePrimary()
346
    {
347
        $this->recurseEnter();
348
        $expr = $this->parsePrimaryStart();
349 View Code Duplication
        while (true) {
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...
350
            if ($this->getCurrentToken()->Id == ExpressionTokenId::SLASH) {
351
                $this->lexer->nextToken();
352
                $expr = $this->parsePropertyAccess($expr);
0 ignored issues
show
Documentation introduced by
$expr is of type object<POData\UriProcess...ons\AbstractExpression>, but the function expects a null|object<POData\UriPr...opertyAccessExpression>.

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...
353
            } else {
354
                break;
355
            }
356
        }
357
358
        $this->recurseLeave();
359
360
        return $expr;
361
    }
362
363
    /**
364
     * Parse primary tokens [literals, identifiers (e.g. function call), open param for sub expressions].
365
     *
366
     *
367
     * @return AbstractExpression
368
     */
369
    private function parsePrimaryStart()
370
    {
371
        switch ($this->lexer->getCurrentToken()->Id) {
372
            case ExpressionTokenId::BOOLEAN_LITERAL:
373
                return $this->parseTypedLiteral(new Boolean());
374
            case ExpressionTokenId::DATETIME_LITERAL:
375
                return $this->parseTypedLiteral(new DateTime());
376
            case ExpressionTokenId::DECIMAL_LITERAL:
377
                return $this->parseTypedLiteral(new Decimal());
378
            case ExpressionTokenId::NULL_LITERAL:
379
                return $this->parseNullLiteral();
380
            case ExpressionTokenId::IDENTIFIER:
381
                return $this->parseIdentifier();
382
            case ExpressionTokenId::STRING_LITERAL:
383
                return $this->parseTypedLiteral(new StringType());
384
            case ExpressionTokenId::INT64_LITERAL:
385
                return $this->parseTypedLiteral(new Int64());
386
            case ExpressionTokenId::INTEGER_LITERAL:
387
                return $this->parseTypedLiteral(new Int32());
388
            case ExpressionTokenId::DOUBLE_LITERAL:
389
                return $this->parseTypedLiteral(new Double());
390
            case ExpressionTokenId::SINGLE_LITERAL:
391
                return $this->parseTypedLiteral(new Single());
392
            case ExpressionTokenId::GUID_LITERAL:
393
                return $this->parseTypedLiteral(new Guid());
394
            case ExpressionTokenId::BINARY_LITERAL:
395
                throw new NotImplementedException(
396
                    'Support for binary is not implemented'
397
                );
398
                //return $this->parseTypedLiteral(new Binary());
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
399
            case ExpressionTokenId::OPENPARAM:
400
                return $this->parseParenExpression();
401
            default:
402
                throw ODataException::createSyntaxError('Expression expected.');
403
        }
404
    }
405
406
    /**
407
     * Parse Sub expression.
408
     *
409
     * @return AbstractExpression
410
     */
411 View Code Duplication
    private function parseParenExpression()
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...
412
    {
413
        if ($this->getCurrentToken()->Id != ExpressionTokenId::OPENPARAM) {
414
            throw ODataException::createSyntaxError('Open parenthesis expected.');
415
        }
416
417
        $this->lexer->nextToken();
418
        $expr = $this->parseExpression();
419
        if ($this->getCurrentToken()->Id != ExpressionTokenId::CLOSEPARAM) {
420
            throw ODataException::createSyntaxError('Close parenthesis expected.');
421
        }
422
423
        $this->lexer->nextToken();
424
425
        return $expr;
426
    }
427
428
    /**
429
     * Parse an identifier.
430
     *
431
     * @return AbstractExpression
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use FunctionCallExpression|PropertyAccessExpression.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
432
     */
433
    private function parseIdentifier()
434
    {
435
        $this->validateToken(ExpressionTokenId::IDENTIFIER);
436
437
        // An open paren here would indicate calling a method
438
        $identifierIsFunction = $this->lexer->peekNextToken()->Id == ExpressionTokenId::OPENPARAM;
439
        if ($identifierIsFunction) {
440
            return $this->parseIdentifierAsFunction();
441
        } else {
442
            return $this->parsePropertyAccess(null);
443
        }
444
    }
445
446
    /**
447
     * Parse a property access.
448
     *
449
     * @param PropertyAccessExpression $parentExpression Parent expression
0 ignored issues
show
Documentation introduced by
Should the type for parameter $parentExpression not be null|PropertyAccessExpression?

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...
450
     *
451
     * @throws ODataException
452
     *
453
     * @return PropertyAccessExpression
454
     */
455
    private function parsePropertyAccess(PropertyAccessExpression $parentExpression = null)
456
    {
457
        $identifier = $this->getCurrentToken()->getIdentifier();
458
        if (is_null($parentExpression)) {
459
            $parentResourceType = $this->resourceType;
460
        } else {
461
            $parentResourceType = $parentExpression->getResourceType();
462
            $this->hasLevel2PropertyInTheExpression = true;
463
        }
464
465
        $resourceProperty = $parentResourceType->resolveProperty($identifier);
466
        if (is_null($resourceProperty)) {
467
            throw ODataException::createSyntaxError(
468
                Messages::expressionLexerNoPropertyInType(
469
                    $identifier,
470
                    $parentResourceType->getFullName(),
471
                    $this->getCurrentToken()->Position
472
                )
473
            );
474
        }
475
476
        if ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCESET_REFERENCE) {
477
            throw ODataException::createSyntaxError(
478
                Messages::expressionParserEntityCollectionNotAllowedInFilter(
479
                    $resourceProperty->getName(),
480
                    $parentResourceType->getFullName(),
481
                    $this->getCurrentToken()->Position
482
                )
483
            );
484
        }
485
486
        $exp = new PropertyAccessExpression($parentExpression, $resourceProperty);
0 ignored issues
show
Bug introduced by
It seems like $parentExpression defined by parameter $parentExpression on line 455 can be null; however, POData\UriProcessor\Quer...pression::__construct() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
487
        $this->lexer->nextToken();
488
489
        return $exp;
490
    }
491
492
    /**
493
     * Try to parse an identifier which is followed by an opern bracket as
494
     * astoria URI function call.
495
     *
496
     * @throws ODataException
497
     *
498
     * @return AbstractExpression
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use FunctionCallExpression.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
499
     */
500
    private function parseIdentifierAsFunction()
501
    {
502
        $functionToken = clone $this->getCurrentToken();
503
        $functions = FunctionDescription::verifyFunctionExists($functionToken);
504
        $this->lexer->nextToken();
505
        $paramExpressions = $this->parseArgumentList();
506
        $function = FunctionDescription::verifyFunctionCallOpArguments(
507
            $functions,
508
            $paramExpressions,
509
            $functionToken
510
        );
511
512
        return new FunctionCallExpression($function, $paramExpressions);
513
    }
514
515
    /**
516
     * Start parsing argument list of a function-call.
517
     *
518
     * @return array<AbstractExpression>
519
     */
520 View Code Duplication
    private function parseArgumentList()
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...
521
    {
522
        if ($this->getCurrentToken()->Id != ExpressionTokenId::OPENPARAM) {
523
            throw ODataException::createSyntaxError('Open parenthesis expected.');
524
        }
525
526
        $this->lexer->nextToken();
527
        $args = $this->getCurrentToken()->Id != ExpressionTokenId::CLOSEPARAM
528
             ? $this->parseArguments() : [];
529
        if ($this->getCurrentToken()->Id != ExpressionTokenId::CLOSEPARAM) {
530
            throw ODataException::createSyntaxError('Close parenthesis expected.');
531
        }
532
533
        $this->lexer->nextToken();
534
535
        return $args;
536
    }
537
538
    /**
539
     * Parse arguments of  a function-call.
540
     *
541
     * @return array<AbstractExpression>
542
     */
543
    private function parseArguments()
544
    {
545
        $argList = [];
546 View Code Duplication
        while (true) {
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...
547
            $argList[] = $this->parseExpression();
548
            if ($this->getCurrentToken()->Id != ExpressionTokenId::COMMA) {
549
                break;
550
            }
551
552
            $this->lexer->nextToken();
553
        }
554
555
        return $argList;
556
    }
557
558
    /**
559
     * Parse primitive type literal.
560
     *
561
     * @param IType $targetType Expected type of the current literal
562
     *
563
     * @throws ODataException
564
     *
565
     * @return AbstractExpression
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use ConstantExpression.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
566
     */
567
    private function parseTypedLiteral(IType $targetType)
568
    {
569
        $literal = $this->lexer->getCurrentToken()->Text;
570
        $outVal = null;
571
        if (!$targetType->validate($literal, $outVal)) {
572
            throw ODataException::createSyntaxError(
573
                Messages::expressionParserUnrecognizedLiteral(
574
                    $targetType->getFullTypeName(),
575
                    $literal,
576
                    $this->lexer->getCurrentToken()->Position
577
                )
578
            );
579
        }
580
581
        $result = new ConstantExpression($outVal, $targetType);
582
        $this->lexer->nextToken();
583
584
        return $result;
585
    }
586
587
    /**
588
     * Parse null literal.
589
     *
590
     * @return ConstantExpression
591
     */
592
    private function parseNullLiteral()
593
    {
594
        $this->lexer->nextToken();
595
596
        return new ConstantExpression(null, new Null1());
597
    }
598
599
    /**
600
     * Check the current token is of a specific kind.
601
     *
602
     * @param ExpressionTokenId $expressionTokenId Token to check
603
     *                                             with current token
604
     *
605
     * @return bool
606
     */
607
    private function tokenIdentifierIs($expressionTokenId)
0 ignored issues
show
Coding Style introduced by
function tokenIdentifierIs() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
608
    {
609
        return $this->getCurrentToken()->identifierIs($expressionTokenId);
610
    }
611
612
    /**
613
     * Validate the current token.
614
     *
615
     * @param ExpressionTokenId $expressionTokenId Token to check
616
     *                                             with current token
617
     *
618
     * @throws ODataException
619
     */
620
    private function validateToken($expressionTokenId)
621
    {
622
        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
     *
631
     * @throws ODataException If max recursion limit hits
632
     */
633
    private function recurseEnter()
634
    {
635
        ++$this->recursionDepth;
636
        if ($this->recursionDepth == self::RECURSION_LIMIT) {
637
            throw ODataException::createSyntaxError('Recursion limit reached.');
638
        }
639
    }
640
641
    /**
642
     * Decrement recursion count.
643
     */
644
    private function recurseLeave()
645
    {
646
        --$this->recursionDepth;
647
    }
648
649
    /**
650
     * Generates Comparison Expression.
651
     *
652
     * @param AbstractExpression $left                    The LHS expression
653
     * @param AbstractExpression $right                   The RHS expression
654
     * @param ExpressionToken    $expressionToken         The comparison expression token
655
     * @param bool               $isPHPExpressionProvider
656
     *
657
     * @return AbstractExpression
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use FunctionCallExpression|U...on|RelationalExpression.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
658
     */
659
    private static function generateComparisonExpression($left, $right, $expressionToken, $isPHPExpressionProvider)
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $isPHPExpressionProvider exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
660
    {
661
        FunctionDescription::verifyRelationalOpArguments($expressionToken, $left, $right);
662
663
        //We need special handling for comparison of following types:
664
        //1. EdmString
665
        //2. DateTime
666
        //3. Guid
667
        //4. Binary
668
        //Will make these comparison as function calls, which will
669
        // be converted to language specific function call by expression
670
        // provider
671
        $string = new StringType();
672 View Code Duplication
        if ($left->typeIs($string) && $right->typeIs($string)) {
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...
673
            $strcmpFunctions = FunctionDescription::stringComparisonFunctions();
674
            $left = new FunctionCallExpression($strcmpFunctions[0], [$left, $right]);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $left. This often makes code more readable.
Loading history...
675
            $right = new ConstantExpression(0, new Int32());
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $right. This often makes code more readable.
Loading history...
676
        }
677
678
        $dateTime = new DateTime();
679 View Code Duplication
        if ($left->typeIs($dateTime) && $right->typeIs($dateTime)) {
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...
680
            $dateTimeCmpFunctions = FunctionDescription::dateTimeComparisonFunctions();
681
            $left = new FunctionCallExpression($dateTimeCmpFunctions[0], [$left, $right]);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $left. This often makes code more readable.
Loading history...
682
            $right = new ConstantExpression(0, new Int32());
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $right. This often makes code more readable.
Loading history...
683
        }
684
685
        $guid = new Guid();
686 View Code Duplication
        if ($left->typeIs($guid) && $right->typeIs($guid)) {
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...
687
            $guidEqualityFunctions = FunctionDescription::guidEqualityFunctions();
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $guidEqualityFunctions exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
688
            $left = new FunctionCallExpression($guidEqualityFunctions[0], [$left, $right]);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $left. This often makes code more readable.
Loading history...
689
            $right = new ConstantExpression(true, new Boolean());
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

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...
Coding Style introduced by
Consider using a different name than the parameter $right. This often makes code more readable.
Loading history...
690
        }
691
692
        $binary = new Binary();
693 View Code Duplication
        if ($left->typeIs($binary) && $right->typeIs($binary)) {
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...
694
            $binaryEqualityFunctions = FunctionDescription::binaryEqualityFunctions();
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $binaryEqualityFunctions exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
695
            $left = new FunctionCallExpression($binaryEqualityFunctions[0], [$left, $right]);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $left. This often makes code more readable.
Loading history...
696
            $right = new ConstantExpression(true, new Boolean());
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

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...
Coding Style introduced by
Consider using a different name than the parameter $right. This often makes code more readable.
Loading history...
697
        }
698
699
        $null = new Null1();
700
        if ($left->typeIs($null) || $right->typeIs($null)) {
701
            // If the end user is responsible for implementing IExpressionProvider
702
            // then the sub-tree for a nullability check would be:
703
704
            //          RelationalExpression(EQ/NE)
705
            //                    |
706
            //               ------------
707
            //               |           |
708
            //               |           |
709
            //            CustomerID    NULL
710
711
            // Otherwise (In case of default PHPExpressionProvider):
712
713
            //  CustomerID eq null
714
            //  ==================
715
716
            //              FunctionCallExpression(is_null)
717
            //                       |
718
            //                       |- Signature => bool (typeof(CustomerID))
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
719
            //                       |- args => {CustomerID}
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
720
721
            //  CustomerID ne null
722
            //  ==================
723
724
            //              UnaryExpression (not)
725
            //                       |
726
            //              FunctionCallExpression(is_null)
727
            //                       |
728
            //                       |- Signature => bool (typeof(CustomerID))
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
729
            //                       |- args => {CustomerID}
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
730
731
            if ($isPHPExpressionProvider) {
732
                $arg = $left->typeIs($null) ? $right : $left;
733
                $isNullFunctionDescription = new FunctionDescription('is_null', new Boolean(), [$arg->getType()]);
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $isNullFunctionDescription exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
734
                switch ($expressionToken->Text) {
735
                    case ODataConstants::KEYWORD_EQUAL:
736
                        return new FunctionCallExpression($isNullFunctionDescription, [$arg]);
737
                        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...
738
739
                    case ODataConstants::KEYWORD_NOT_EQUAL:
740
                        return new UnaryExpression(
741
                            new FunctionCallExpression($isNullFunctionDescription, [$arg]),
742
                            ExpressionType::NOT_LOGICAL,
743
                            new Boolean()
744
                        );
745
                        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...
746
                }
747
            }
748
        }
749
750
        switch ($expressionToken->Text) {
751
            case ODataConstants::KEYWORD_EQUAL:
752
                return new RelationalExpression(
753
                    $left,
754
                    $right,
755
                    ExpressionType::EQUAL
756
                );
757
            case ODataConstants::KEYWORD_NOT_EQUAL:
758
                return new RelationalExpression(
759
                    $left,
760
                    $right,
761
                    ExpressionType::NOTEQUAL
762
                );
763
            case ODataConstants::KEYWORD_GREATERTHAN:
764
                return new RelationalExpression(
765
                    $left,
766
                    $right,
767
                    ExpressionType::GREATERTHAN
768
                );
769
            case ODataConstants::KEYWORD_GREATERTHAN_OR_EQUAL:
770
                return new RelationalExpression(
771
                    $left,
772
                    $right,
773
                    ExpressionType::GREATERTHAN_OR_EQUAL
774
                );
775
            case ODataConstants::KEYWORD_LESSTHAN:
776
                return new RelationalExpression(
777
                    $left,
778
                    $right,
779
                    ExpressionType::LESSTHAN
780
                );
781
            default:
782
                return new RelationalExpression(
783
                    $left,
784
                    $right,
785
                    ExpressionType::LESSTHAN_OR_EQUAL
786
                );
787
        }
788
    }
789
}
790