Passed
Branch master (950424)
by Christopher
11:06
created

FunctionDescription::notOperationFunctions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace POData\UriProcessor\QueryProcessor;
4
5
use POData\Common\Messages;
6
use POData\Common\ODataConstants;
7
use POData\Common\ODataException;
8
use POData\Providers\Metadata\Type\Binary;
9
use POData\Providers\Metadata\Type\Boolean;
10
use POData\Providers\Metadata\Type\DateTime;
11
use POData\Providers\Metadata\Type\Decimal;
12
use POData\Providers\Metadata\Type\Double;
13
use POData\Providers\Metadata\Type\Guid;
14
use POData\Providers\Metadata\Type\Int16;
15
use POData\Providers\Metadata\Type\Int32;
16
use POData\Providers\Metadata\Type\Int64;
17
use POData\Providers\Metadata\Type\IType;
18
use POData\Providers\Metadata\Type\Null1;
19
use POData\Providers\Metadata\Type\Single;
20
use POData\Providers\Metadata\Type\StringType;
21
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\AbstractExpression;
22
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionToken;
23
24
/**
25
 * Class FunctionDescription.
26
 *
27
 * Class to represent function signature including function-name
28
 */
29
class FunctionDescription
30
{
31
    /**
32
     * @var string
33
     */
34
    public $name;
35
36
    /**
37
     * @var IType
38
     */
39
    public $returnType;
40
41
    /**
42
     * @var IType[]
43
     */
44
    public $argumentTypes;
45
46
    /**
47
     * Create new instance of FunctionDescription.
48
     *
49
     * @param string  $name          Name of the function
50
     * @param IType   $returnType    Return type
51
     * @param IType[] $argumentTypes Parameter type
52
     */
53
    public function __construct($name, $returnType, $argumentTypes)
54
    {
55
        $this->name = $name;
56
        $this->returnType = $returnType;
57
        $this->argumentTypes = $argumentTypes;
58
    }
59
60
    /**
61
     * Get the function prototype as string.
62
     *
63
     * @return string
64
     */
65
    public function getPrototypeAsString()
66
    {
67
        $str = $this->returnType->getFullTypeName() . ' ' . $this->name . '(';
68
69
        foreach ($this->argumentTypes as $argumentType) {
70
            $str .= $argumentType->getFullTypeName() . ', ';
71
        }
72
73
        return rtrim($str, ', ') . ')';
74
    }
75
76
    /**
77
     * Create function descriptions for supported function-calls in $filter option.
78
     *
79
     * TODO: FIGURE OUT WHAT THE HECK THIS IS RETURNING!?!?
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...
80
     *
81
     * @return array<string,FunctionDescription[]> indexed by function name
82
     */
83
    public static function filterFunctionDescriptions()
84
    {
85
        $functions = [
86
            //EdmString Functions
87
            'endswith' => [
88
                    new self(
89
                        'endswith',
90
                        new Boolean(),
91
                        [new StringType(), new StringType()]
92
                    ),
93
                ],
94
            'indexof' => [
95
                    new self(
96
                        'indexof',
97
                        new Int32(),
98
                        [new StringType(), new StringType()]
99
                    ),
100
                ],
101
            'replace' => [
102
                    new self(
103
                        'replace',
104
                        new StringType(),
105
                        [new StringType(), new StringType(), new StringType()]
106
                    ),
107
                ],
108
            'startswith' => [
109
                    new self(
110
                        'startswith',
111
                        new Boolean(),
112
                        [new StringType(), new StringType()]
113
                    ),
114
                ],
115
            'tolower' => [
116
                    new self(
117
                        'tolower',
118
                        new StringType(),
119
                        [new StringType()]
120
                    ),
121
                ],
122
            'toupper' => [
123
                    new self(
124
                        'toupper',
125
                        new StringType(),
126
                        [new StringType()]
127
                    ),
128
                ],
129
            'trim' => [
130
                    new self(
131
                        'trim',
132
                        new StringType(),
133
                        [new StringType()]
134
                    ),
135
                ],
136
            'substring' => [
137
                    new self(
138
                        'substring',
139
                        new StringType(),
140
                        [new StringType(), new Int32()]
141
                    ),
142
                    new self(
143
                        'substring',
144
                        new StringType(),
145
                        [new StringType(), new Int32(), new Int32()]
146
                    ),
147
                ],
148
            'substringof' => [
149
                    new self(
150
                        'substringof',
151
                        new Boolean(),
152
                        [new StringType(), new StringType()]
153
                    ),
154
                ],
155
            'concat' => [
156
                    new self(
157
                        'concat',
158
                        new StringType(),
159
                        [new StringType(), new StringType()]
160
                    ),
161
                ],
162
            'length' => [
163
                    new self(
164
                        'length',
165
                        new Int32(),
166
                        [new StringType()]
167
                    ),
168
                ],
169
            //DateTime functions
170
            'year' => [
171
                    new self(
172
                        'year',
173
                        new Int32(),
174
                        [new DateTime()]
175
                    ),
176
                ],
177
            'month' => [
178
                    new self(
179
                        'month',
180
                        new Int32(),
181
                        [new DateTime()]
182
                    ),
183
                ],
184
            'day' => [
185
                    new self(
186
                        'day',
187
                        new Int32(),
188
                        [new DateTime()]
189
                    ),
190
                ],
191
            'hour' => [
192
                    new self(
193
                        'hour',
194
                        new Int32(),
195
                        [new DateTime()]
196
                    ),
197
                ],
198
            'minute' => [
199
                    new self(
200
                        'minute',
201
                        new Int32(),
202
                        [new DateTime()]
203
                    ),
204
                ],
205
            'second' => [
206
                    new self(
207
                        'second',
208
                        new Int32(),
209
                        [new DateTime()]
210
                    ),
211
                ],
212
            //Math Functions
213
            'round' => [
214
                    new self(
215
                        'round',
216
                        new Decimal(),
217
                        [new Decimal()]
218
                    ),
219
                    new self(
220
                        'round',
221
                        new Double(),
222
                        [new Double()]
223
                    ),
224
                ],
225
            'ceiling' => [
226
                    new self(
227
                        'ceiling',
228
                        new Decimal(),
229
                        [new Decimal()]
230
                    ),
231
                    new self(
232
                        'ceiling',
233
                        new Double(),
234
                        [new Double()]
235
                    ),
236
                ],
237
            'floor' => [
238
                    new self(
239
                        'floor',
240
                        new Decimal(),
241
                        [new Decimal()]
242
                    ),
243
                    new self(
244
                        'floor',
245
                        new Double(),
246
                        [new Double()]
247
                    ),
248
                ],
249
            ];
250
251
        return $functions;
252
    }
253
254
    /**
255
     * Get function description for string comparison.
256
     *
257
     * @return FunctionDescription[]
258
     */
259
    public static function stringComparisonFunctions()
260
    {
261
        return [
262
            new self(
263
                'strcmp',
264
                new Int32(),
265
                [new StringType(), new StringType()]
266
            ),
267
        ];
268
    }
269
270
    /**
271
     * Get function description for datetime comparison.
272
     *
273
     * @return FunctionDescription[]
274
     */
275
    public static function dateTimeComparisonFunctions()
276
    {
277
        return [
278
            new self(
279
                'dateTimeCmp',
280
                new Int32(),
281
                [new DateTime(), new DateTime()]
282
            ),
283
        ];
284
    }
285
286
    /**
287
     * Get function description for guid equality check.
288
     *
289
     * @return FunctionDescription[]
290
     */
291
    public static function guidEqualityFunctions()
292
    {
293
        return [
294
            new self(
295
                'guidEqual',
296
                new Boolean(),
297
                [new Guid(), new Guid()]
298
            ),
299
        ];
300
    }
301
302
    /**
303
     * Get function description for binary equality check.
304
     *
305
     * @return FunctionDescription[]
306
     */
307
    public static function binaryEqualityFunctions()
308
    {
309
        return [
310
            new self(
311
                'binaryEqual',
312
                new Boolean(),
313
                [new Binary(), new Binary()]
314
            ),
315
        ];
316
    }
317
318
    /**
319
     * Get function descriptions for arithmetic operations.
320
     *
321
     * @return FunctionDescription[]
322
     */
323
    public static function arithmeticOperationFunctions()
324
    {
325
        return [
326
            new self(
327
                'F',
328
                new Int16(),
329
                [new Int16(), new Int16()]
330
            ),
331
            new self(
332
                'F',
333
                new Int32(),
334
                [new Int32(), new Int32()]
335
            ),
336
            new self(
337
                'F',
338
                new Int64(),
339
                [new Int64(), new Int64()]
340
            ),
341
            new self(
342
                'F',
343
                new Single(),
344
                [new Single(), new Single()]
345
            ),
346
            new self(
347
                'F',
348
                new Double(),
349
                [new Double(), new Double()]
350
            ),
351
            new self(
352
                'F',
353
                new Decimal(),
354
                [new Decimal(), new Decimal()]
355
            ),
356
        ];
357
    }
358
359
    /**
360
     * Get function descriptions for arithmetic add operations.
361
     *
362
     * @return FunctionDescription[] indexed by function name
363
     */
364
    public static function addOperationFunctions()
365
    {
366
        return self::arithmeticOperationFunctions();
367
    }
368
369
    /**
370
     * Get function descriptions for arithmetic subtract operations.
371
     *
372
     * @return FunctionDescription[] indexed by function name
373
     */
374
    public static function subtractOperationFunctions()
375
    {
376
        return self::arithmeticOperationFunctions();
377
    }
378
379
    /**
380
     * Get function descriptions for logical operations.
381
     *
382
     * @return FunctionDescription[]
383
     */
384
    public static function logicalOperationFunctions()
385
    {
386
        return [
387
            new self(
388
                'F',
389
                new Boolean(),
390
                [new Boolean(), new Boolean()]
391
            ),
392
        ];
393
    }
394
395
    /**
396
     * Get function descriptions for relational operations.
397
     *
398
     * @return FunctionDescription[]
399
     */
400
    public static function relationalOperationFunctions()
401
    {
402
        return array_merge(
403
            self::arithmeticOperationFunctions(),
404
            [
405
                new self(
406
                    'F',
407
                    new Boolean(),
408
                    [new Boolean(), new Boolean()]
409
                ),
410
                new self(
411
                    'F',
412
                    new DateTime(),
413
                    [new DateTime(), new DateTime()]
414
                ),
415
                new self(
416
                    'F',
417
                    new Guid(),
418
                    [new Guid(), new Guid()]
419
                ),
420
                new self(
421
                    'F',
422
                    new Boolean(),
423
                    [new Binary(), new Binary()]
424
                ),
425
            ]
426
        );
427
    }
428
429
    /**
430
     * Get function descriptions for unary not operation.
431
     *
432
     * @return FunctionDescription[]
433
     */
434
    public static function notOperationFunctions()
435
    {
436
        return [
437
            new self(
438
                'F',
439
                new Boolean(),
440
                [new Boolean()]
441
            ),
442
        ];
443
    }
444
445
    /**
446
     * Get function description for checking an operand is null or not.
447
     *
448
     * @param IType $type Type of the argument to null check function
449
     *
450
     * @return \POData\UriProcessor\QueryProcessor\FunctionDescription
451
     */
452
    public static function isNullCheckFunction(IType $type)
453
    {
454
        return new self('is_null', new Boolean(), [$type]);
455
    }
456
457
    /**
458
     * Get function description for unary negate operator.
459
     *
460
     * @return FunctionDescription[]
461
     */
462
    public static function negateOperationFunctions()
463
    {
464
        return [
465
            new self('F', new Int16(), [new Int16()]),
466
            new self('F', new Int32(), [new Int32()]),
467
            new self('F', new Int64(), [new Int64()]),
468
            new self('F', new Single(), [new Single()]),
469
            new self('F', new Double(), [new Double()]),
470
            new self('F', new Decimal(), [new Decimal()]),
471
        ];
472
    }
473
474
    /**
475
     * To throw ODataException for incompatible types.
476
     *
477
     * @param ExpressionToken      $expressionToken Expression token
478
     * @param AbstractExpression[] $argExpressions  Array of argument expression
479
     *
480
     * @throws ODataException
481
     */
482
    public static function incompatibleError($expressionToken, $argExpressions)
483
    {
484
        $string = null;
485
        foreach ($argExpressions as $argExpression) {
486
            $string .= $argExpression->getType()->getFullTypeName() . ', ';
487
        }
488
489
        $string = rtrim($string, ', ');
490
        $pos = strrpos($string, ', ');
491
        if ($pos !== false) {
492
            $string = substr_replace($string, ' and ', strrpos($string, ', '), 2);
0 ignored issues
show
Bug introduced by
It seems like strrpos($string, ', ') can also be of type false; however, parameter $start of substr_replace() does only seem to accept integer, 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

492
            $string = substr_replace($string, ' and ', /** @scrutinizer ignore-type */ strrpos($string, ', '), 2);
Loading history...
493
        }
494
495
        throw ODataException::createSyntaxError(
496
            Messages::expressionParserInCompatibleTypes(
497
                $expressionToken->Text,
498
                $string,
499
                $expressionToken->Position
500
            )
501
        );
502
    }
503
504
    /**
505
     * Validate operands of an arithmetic operation and promote if required.
506
     *
507
     * @param ExpressionToken    $expressionToken The expression token
508
     * @param AbstractExpression $leftArgument    The left expression
509
     * @param AbstractExpression $rightArgument   The right expression
510
     *
511
     * @return IType
512
     */
513 View Code Duplication
    public static function verifyAndPromoteArithmeticOpArguments(
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...
514
        $expressionToken,
515
        $leftArgument,
516
        $rightArgument
517
    ) {
518
        $function
519
            = self::findFunctionWithPromotion(
520
                self::arithmeticOperationFunctions(),
521
                [$leftArgument, $rightArgument]
522
            );
523
        if ($function == null) {
524
            self::incompatibleError(
525
                $expressionToken,
526
                [$leftArgument, $rightArgument]
527
            );
528
        }
529
530
        return $function->returnType;
531
    }
532
533
    /**
534
     * Validate operands of an logical operation.
535
     *
536
     * @param ExpressionToken    $expressionToken The expression token
537
     * @param AbstractExpression $leftArgument    The left expression
538
     * @param AbstractExpression $rightArgument   The right expression
539
     *
540
     * @throws ODataException
541
     */
542 View Code Duplication
    public static function verifyLogicalOpArguments(
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...
543
        $expressionToken,
544
        $leftArgument,
545
        $rightArgument
546
    ) {
547
        $function = self::findFunctionWithPromotion(
548
            self::logicalOperationFunctions(),
549
            [$leftArgument, $rightArgument],
550
            false
551
        );
552
        if ($function == null) {
553
            self::incompatibleError(
554
                $expressionToken,
555
                [$leftArgument, $rightArgument]
556
            );
557
        }
558
    }
559
560
    /**
561
     * Validate operands of an relational operation.
562
     *
563
     * @param ExpressionToken    $expressionToken The expression token
564
     * @param AbstractExpression $leftArgument    The left argument expression
565
     * @param AbstractExpression $rightArgument   The right argument expression
566
     *
567
     * @throws ODataException
568
     */
569
    public static function verifyRelationalOpArguments(
570
        $expressionToken,
571
        $leftArgument,
572
        $rightArgument
573
    ) {
574
        //for null operands only equality operators are allowed
575
        $null = new Null1();
576 View Code Duplication
        if ($leftArgument->typeIs($null) || $rightArgument->typeIs($null)) {
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...
577
            if ((strcmp($expressionToken->Text, ODataConstants::KEYWORD_EQUAL) != 0)
578
                && (strcmp($expressionToken->Text, ODataConstants::KEYWORD_NOT_EQUAL) != 0)
579
            ) {
580
                throw ODataException::createSyntaxError(
581
                    Messages::expressionParserOperatorNotSupportNull(
582
                        $expressionToken->Text,
583
                        $expressionToken->Position
584
                    )
585
                );
586
            }
587
588
            return;
589
        }
590
591
        //for guid operands only equality operators are allowed
592
        $guid = new Guid();
593 View Code Duplication
        if ($leftArgument->typeIs($guid) && $rightArgument->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...
594
            if ((strcmp($expressionToken->Text, ODataConstants::KEYWORD_EQUAL) != 0)
595
                && (strcmp($expressionToken->Text, ODataConstants::KEYWORD_NOT_EQUAL) != 0)
596
            ) {
597
                throw ODataException::createSyntaxError(
598
                    Messages::expressionParserOperatorNotSupportGuid(
599
                        $expressionToken->Text,
600
                        $expressionToken->Position
601
                    )
602
                );
603
            }
604
605
            return;
606
        }
607
608
        //for binary operands only equality operators are allowed
609
        $binary = new Binary();
610 View Code Duplication
        if ($leftArgument->typeIs($binary) && $rightArgument->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...
611
            if ((strcmp($expressionToken->Text, ODataConstants::KEYWORD_EQUAL) != 0)
612
                && (strcmp($expressionToken->Text, ODataConstants::KEYWORD_NOT_EQUAL) != 0)
613
            ) {
614
                throw ODataException::createSyntaxError(
615
                    Messages::expressionParserOperatorNotSupportBinary(
616
                        $expressionToken->Text,
617
                        $expressionToken->Position
618
                    )
619
                );
620
            }
621
622
            return;
623
        }
624
625
        //TODO: eq and ne is valid for 'resource reference'
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...
626
        //navigation also verify here
627
628
        $functions = array_merge(
629
            self::relationalOperationFunctions(),
630
            self::stringComparisonFunctions()
631
        );
632
        $function = self::findFunctionWithPromotion(
633
            $functions,
634
            [$leftArgument, $rightArgument],
635
            false
636
        );
637
        if ($function == null) {
638
            self::incompatibleError(
639
                $expressionToken,
640
                [$leftArgument, $rightArgument]
641
            );
642
        }
643
    }
644
645
    /**
646
     * Validate operands of a unary  operation.
647
     *
648
     * @param ExpressionToken    $expressionToken The expression token
649
     * @param AbstractExpression $argExpression   Argument expression
650
     *
651
     * @throws ODataException
652
     */
653
    public static function validateUnaryOpArguments($expressionToken, $argExpression)
654
    {
655
        //Unary not
656 View Code Duplication
        if (strcmp($expressionToken->Text, ODataConstants::KEYWORD_NOT) == 0) {
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...
657
            $function = self::findFunctionWithPromotion(
658
                self::notOperationFunctions(),
659
                [$argExpression]
660
            );
661
            if ($function == null) {
662
                self::incompatibleError($expressionToken, [$argExpression]);
663
            }
664
665
            return;
666
        }
667
668
        //Unary minus (negation)
669 View Code Duplication
        if (strcmp($expressionToken->Text, '-') == 0) {
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...
670
            if (self::findFunctionWithPromotion(self::negateOperationFunctions(), [$argExpression]) == null) {
671
                self::incompatibleError($expressionToken, [$argExpression]);
672
            }
673
        }
674
    }
675
676
    /**
677
     * Check am identifier is a valid filter function.
678
     *
679
     * @param ExpressionToken $expressionToken The expression token
680
     *
681
     * @throws ODataException
682
     *
683
     * @return FunctionDescription[] Array of matching functions
684
     */
685
    public static function verifyFunctionExists($expressionToken)
686
    {
687
        if (!array_key_exists($expressionToken->Text, self::filterFunctionDescriptions())) {
688
            throw ODataException::createSyntaxError(
689
                Messages::expressionParserUnknownFunction(
690
                    $expressionToken->Text,
691
                    $expressionToken->Position
692
                )
693
            );
694
        }
695
696
        $filterFunctions = self::filterFunctionDescriptions();
697
698
        return $filterFunctions[$expressionToken->Text];
699
    }
700
701
    /**
702
     * Validate operands (arguments) of a function call operation and return
703
     * matching function.
704
     *
705
     * @param \POData\UriProcessor\QueryProcessor\FunctionDescription[] $functions       List of functions to be checked
706
     * @param AbstractExpression[]                                      $argExpressions  Function argument expressions
707
     * @param ExpressionToken                                           $expressionToken Expression token
708
     *
709
     * @throws ODataException
710
     *
711
     * @return \POData\UriProcessor\QueryProcessor\FunctionDescription
712
     */
713
    public static function verifyFunctionCallOpArguments(
714
        $functions,
715
        $argExpressions,
716
        $expressionToken
717
    ) {
718
        $function
719
            = self::findFunctionWithPromotion($functions, $argExpressions, false);
720
        if ($function == null) {
721
            $protoTypes = null;
722
            foreach ($functions as $function) {
723
                $protoTypes .= $function->getPrototypeAsString() . '; ';
724
            }
725
726
            throw ODataException::createSyntaxError(
727
                Messages::expressionLexerNoApplicableFunctionsFound(
728
                    $expressionToken->Text,
729
                    $protoTypes,
730
                    $expressionToken->Position
731
                )
732
            );
733
        }
734
735
        return $function;
736
    }
737
738
    /**
739
     * Finds a function from the list of functions whose argument types matches
740
     * with types of expressions.
741
     *
742
     * @param FunctionDescription[] $functionDescriptions List of functions
743
     * @param AbstractExpression[]  $argExpressions       Function argument expressions
744
     * @param bool                  $promoteArguments     Function argument
745
     *
746
     * @return FunctionDescription|null Reference to the matching function if found else NULL
747
     */
748
    public static function findFunctionWithPromotion(
749
        $functionDescriptions,
750
        $argExpressions,
751
        $promoteArguments = true
752
    ) {
753
        $argCount = count($argExpressions);
754
        $applicableFunctions = [];
755
        foreach ($functionDescriptions as $functionDescription) {
756
            if (count($functionDescription->argumentTypes) == $argCount) {
757
                $applicableFunctions[] = $functionDescription;
758
            }
759
        }
760
761
        if (empty($applicableFunctions)) {
762
            return null;
763
        }
764
765
        //Check for exact match
766
        foreach ($applicableFunctions as $function) {
767
            $i = 0;
768
            foreach ($function->argumentTypes as $argumentType) {
769
                if (!$argExpressions[$i]->typeIs($argumentType)) {
770
                    break;
771
                }
772
773
                ++$i;
774
            }
775
776
            if ($i == $argCount) {
777
                return $function;
778
            }
779
        }
780
781
        //Check match with promotion
782
        foreach ($applicableFunctions as $function) {
783
            $i = 0;
784
            $promotedTypes = [];
785
            foreach ($function->argumentTypes as $argumentType) {
786
                if (!$argumentType->isCompatibleWith($argExpressions[$i]->getType())) {
787
                    break;
788
                }
789
790
                $promotedTypes[] = $argumentType;
791
                ++$i;
792
            }
793
794
            if ($i == $argCount) {
795
                $i = 0;
796
                if ($promoteArguments) {
797
                    //Promote Argument Expressions
798
                    foreach ($argExpressions as $expression) {
799
                        $expression->setType($promotedTypes[$i++]);
800
                    }
801
                }
802
803
                return $function;
804
            }
805
        }
806
        return null;
807
    }
808
}
809