analyzeNonDivOperands()   F
last analyzed

Complexity

Conditions 130
Paths 724

Size

Total Lines 396

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 130
nc 724
nop 15
dl 0
loc 396
rs 0.3066
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\Expression\BinaryOp;
3
4
use PhpParser;
5
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\ArrayAssignmentAnalyzer;
6
use Psalm\Internal\Analyzer\StatementsAnalyzer;
7
use Psalm\CodeLocation;
8
use Psalm\Config;
9
use Psalm\Context;
10
use Psalm\Issue\FalseOperand;
11
use Psalm\Issue\InvalidOperand;
12
use Psalm\Issue\MixedOperand;
13
use Psalm\Issue\NullOperand;
14
use Psalm\Issue\PossiblyFalseOperand;
15
use Psalm\Issue\PossiblyInvalidOperand;
16
use Psalm\Issue\PossiblyNullOperand;
17
use Psalm\Issue\StringIncrement;
18
use Psalm\IssueBuffer;
19
use Psalm\StatementsSource;
20
use Psalm\Type;
21
use Psalm\Type\Atomic\ObjectLike;
22
use Psalm\Type\Atomic\TArray;
23
use Psalm\Type\Atomic\TFalse;
24
use Psalm\Type\Atomic\TFloat;
25
use Psalm\Type\Atomic\TList;
26
use Psalm\Type\Atomic\TTemplateParam;
27
use Psalm\Type\Atomic\TInt;
28
use Psalm\Type\Atomic\TMixed;
29
use Psalm\Type\Atomic\TNamedObject;
30
use Psalm\Type\Atomic\TNull;
31
use Psalm\Type\Atomic\TNumeric;
32
use Psalm\Internal\Type\TypeCombination;
33
use function array_diff_key;
34
use function array_values;
35
use function strtolower;
36
37
/**
38
 * @internal
39
 */
40
class NonDivArithmeticOpAnalyzer
41
{
42
    public static function analyze(
43
        ?StatementsSource $statements_source,
44
        \Psalm\Internal\Provider\NodeDataProvider $nodes,
45
        PhpParser\Node\Expr $left,
46
        PhpParser\Node\Expr $right,
47
        PhpParser\Node $parent,
48
        ?Type\Union &$result_type = null,
49
        ?Context $context = null
50
    ) : void {
51
        $codebase = $statements_source ? $statements_source->getCodebase() : null;
52
53
        $left_type = $nodes->getType($left);
54
        $right_type = $nodes->getType($right);
55
        $config = Config::getInstance();
56
57
        if ($left_type && $right_type) {
58
            if ($left_type->isNull()) {
59
                if ($statements_source && IssueBuffer::accepts(
60
                    new NullOperand(
61
                        'Left operand cannot be null',
62
                        new CodeLocation($statements_source, $left)
63
                    ),
64
                    $statements_source->getSuppressedIssues()
65
                )) {
66
                    // fall through
67
                }
68
69
                return;
70
            }
71
72
            if ($left_type->isNullable() && !$left_type->ignore_nullable_issues) {
73
                if ($statements_source && IssueBuffer::accepts(
74
                    new PossiblyNullOperand(
75
                        'Left operand cannot be nullable, got ' . $left_type,
76
                        new CodeLocation($statements_source, $left)
77
                    ),
78
                    $statements_source->getSuppressedIssues()
79
                )) {
80
                    // fall through
81
                }
82
            }
83
84
            if ($right_type->isNull()) {
85
                if ($statements_source && IssueBuffer::accepts(
86
                    new NullOperand(
87
                        'Right operand cannot be null',
88
                        new CodeLocation($statements_source, $right)
89
                    ),
90
                    $statements_source->getSuppressedIssues()
91
                )) {
92
                    // fall through
93
                }
94
95
                return;
96
            }
97
98
            if ($right_type->isNullable() && !$right_type->ignore_nullable_issues) {
99
                if ($statements_source && IssueBuffer::accepts(
100
                    new PossiblyNullOperand(
101
                        'Right operand cannot be nullable, got ' . $right_type,
102
                        new CodeLocation($statements_source, $right)
103
                    ),
104
                    $statements_source->getSuppressedIssues()
105
                )) {
106
                    // fall through
107
                }
108
            }
109
110
            if ($left_type->isFalse()) {
111
                if ($statements_source && IssueBuffer::accepts(
112
                    new FalseOperand(
113
                        'Left operand cannot be false',
114
                        new CodeLocation($statements_source, $left)
115
                    ),
116
                    $statements_source->getSuppressedIssues()
117
                )) {
118
                    // fall through
119
                }
120
121
                return;
122
            }
123
124
            if ($left_type->isFalsable() && !$left_type->ignore_falsable_issues) {
125
                if ($statements_source && IssueBuffer::accepts(
126
                    new PossiblyFalseOperand(
127
                        'Left operand cannot be falsable, got ' . $left_type,
128
                        new CodeLocation($statements_source, $left)
129
                    ),
130
                    $statements_source->getSuppressedIssues()
131
                )) {
132
                    // fall through
133
                }
134
            }
135
136
            if ($right_type->isFalse()) {
137
                if ($statements_source && IssueBuffer::accepts(
138
                    new FalseOperand(
139
                        'Right operand cannot be false',
140
                        new CodeLocation($statements_source, $right)
141
                    ),
142
                    $statements_source->getSuppressedIssues()
143
                )) {
144
                    // fall through
145
                }
146
147
                return;
148
            }
149
150
            if ($right_type->isFalsable() && !$right_type->ignore_falsable_issues) {
151
                if ($statements_source && IssueBuffer::accepts(
152
                    new PossiblyFalseOperand(
153
                        'Right operand cannot be falsable, got ' . $right_type,
154
                        new CodeLocation($statements_source, $right)
155
                    ),
156
                    $statements_source->getSuppressedIssues()
157
                )) {
158
                    // fall through
159
                }
160
            }
161
162
            $invalid_left_messages = [];
163
            $invalid_right_messages = [];
164
            $has_valid_left_operand = false;
165
            $has_valid_right_operand = false;
166
            $has_string_increment = false;
167
168
            foreach ($left_type->getAtomicTypes() as $left_type_part) {
169
                foreach ($right_type->getAtomicTypes() as $right_type_part) {
170
                    $candidate_result_type = self::analyzeNonDivOperands(
171
                        $statements_source,
172
                        $codebase,
173
                        $config,
174
                        $context,
175
                        $left,
176
                        $right,
177
                        $parent,
178
                        $left_type_part,
179
                        $right_type_part,
180
                        $invalid_left_messages,
181
                        $invalid_right_messages,
182
                        $has_valid_left_operand,
183
                        $has_valid_right_operand,
184
                        $has_string_increment,
185
                        $result_type
186
                    );
187
188
                    if ($candidate_result_type) {
189
                        $result_type = $candidate_result_type;
190
                        return;
191
                    }
192
                }
193
            }
194
195
            if ($invalid_left_messages && $statements_source) {
196
                $first_left_message = $invalid_left_messages[0];
197
198
                if ($has_valid_left_operand) {
199
                    if (IssueBuffer::accepts(
200
                        new PossiblyInvalidOperand(
201
                            $first_left_message,
202
                            new CodeLocation($statements_source, $left)
203
                        ),
204
                        $statements_source->getSuppressedIssues()
205
                    )) {
206
                        // fall through
207
                    }
208
                } else {
209
                    if (IssueBuffer::accepts(
210
                        new InvalidOperand(
211
                            $first_left_message,
212
                            new CodeLocation($statements_source, $left)
213
                        ),
214
                        $statements_source->getSuppressedIssues()
215
                    )) {
216
                        // fall through
217
                    }
218
                }
219
            }
220
221
            if ($invalid_right_messages && $statements_source) {
222
                $first_right_message = $invalid_right_messages[0];
223
224
                if ($has_valid_right_operand) {
225
                    if (IssueBuffer::accepts(
226
                        new PossiblyInvalidOperand(
227
                            $first_right_message,
228
                            new CodeLocation($statements_source, $right)
229
                        ),
230
                        $statements_source->getSuppressedIssues()
231
                    )) {
232
                        // fall through
233
                    }
234
                } else {
235
                    if (IssueBuffer::accepts(
236
                        new InvalidOperand(
237
                            $first_right_message,
238
                            new CodeLocation($statements_source, $right)
239
                        ),
240
                        $statements_source->getSuppressedIssues()
241
                    )) {
242
                        // fall through
243
                    }
244
                }
245
            }
246
247
            if ($has_string_increment && $statements_source) {
248
                if (IssueBuffer::accepts(
249
                    new StringIncrement(
250
                        'Possibly unintended string increment',
251
                        new CodeLocation($statements_source, $left)
252
                    ),
253
                    $statements_source->getSuppressedIssues()
254
                )) {
255
                    // fall through
256
                }
257
            }
258
        }
259
    }
260
261
    /**
262
     * @param  string[]        &$invalid_left_messages
263
     * @param  string[]        &$invalid_right_messages
264
     *
265
     * @return Type\Union|null
266
     */
267
    private static function analyzeNonDivOperands(
268
        ?StatementsSource $statements_source,
269
        ?\Psalm\Codebase $codebase,
270
        Config $config,
271
        ?Context $context,
272
        PhpParser\Node\Expr $left,
273
        PhpParser\Node\Expr $right,
274
        PhpParser\Node $parent,
275
        Type\Atomic $left_type_part,
276
        Type\Atomic $right_type_part,
277
        array &$invalid_left_messages,
278
        array &$invalid_right_messages,
279
        bool &$has_valid_left_operand,
280
        bool &$has_valid_right_operand,
281
        bool &$has_string_increment,
282
        Type\Union &$result_type = null
283
    ) {
284
        if ($left_type_part instanceof TNull || $right_type_part instanceof TNull) {
285
            // null case is handled above
286
            return;
287
        }
288
289
        if ($left_type_part instanceof TFalse || $right_type_part instanceof TFalse) {
290
            // null case is handled above
291
            return;
292
        }
293
294
        if ($left_type_part instanceof Type\Atomic\TString
295
            && $right_type_part instanceof TInt
296
            && $parent instanceof PhpParser\Node\Expr\PostInc
297
        ) {
298
            $has_string_increment = true;
299
300
            if (!$result_type) {
301
                $result_type = Type::getString();
302
            } else {
303
                $result_type = Type::combineUnionTypes(Type::getString(), $result_type);
304
            }
305
306
            $has_valid_left_operand = true;
307
            $has_valid_right_operand = true;
308
309
            return;
310
        }
311
312
        if ($left_type_part instanceof TTemplateParam
313
            && $right_type_part instanceof TTemplateParam
314
        ) {
315
            $combined_type = Type::combineUnionTypes(
316
                $left_type_part->as,
317
                $right_type_part->as
318
            );
319
320
            $combined_atomic_types = array_values($combined_type->getAtomicTypes());
321
322
            if (\count($combined_atomic_types) <= 2) {
323
                $left_type_part = $combined_atomic_types[0];
324
                $right_type_part = $combined_atomic_types[1] ?? $combined_atomic_types[0];
325
            }
326
        }
327
328
        if ($left_type_part instanceof TMixed
329
            || $right_type_part instanceof TMixed
330
            || $left_type_part instanceof TTemplateParam
331
            || $right_type_part instanceof TTemplateParam
332
        ) {
333
            if ($statements_source && $codebase && $context) {
334
                if (!$context->collect_initializations
335
                    && !$context->collect_mutations
336
                    && $statements_source->getFilePath() === $statements_source->getRootFilePath()
337
                    && (!(($source = $statements_source->getSource())
338
                            instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
339
                        || !$source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
340
                ) {
341
                    $codebase->analyzer->incrementMixedCount($statements_source->getFilePath());
342
                }
343
            }
344
345
            if ($left_type_part instanceof TMixed || $left_type_part instanceof TTemplateParam) {
346
                if ($statements_source && IssueBuffer::accepts(
347
                    new MixedOperand(
348
                        'Left operand cannot be mixed',
349
                        new CodeLocation($statements_source, $left)
350
                    ),
351
                    $statements_source->getSuppressedIssues()
352
                )) {
353
                    // fall through
354
                }
355
            } else {
356
                if ($statements_source && IssueBuffer::accepts(
357
                    new MixedOperand(
358
                        'Right operand cannot be mixed',
359
                        new CodeLocation($statements_source, $right)
360
                    ),
361
                    $statements_source->getSuppressedIssues()
362
                )) {
363
                    // fall through
364
                }
365
            }
366
367
            if ($left_type_part instanceof TMixed
368
                && $left_type_part->from_loop_isset
369
                && $parent instanceof PhpParser\Node\Expr\AssignOp\Plus
370
                && !$right_type_part instanceof TMixed
371
            ) {
372
                $result_type_member = new Type\Union([$right_type_part]);
373
374
                if (!$result_type) {
375
                    $result_type = $result_type_member;
376
                } else {
377
                    $result_type = Type::combineUnionTypes($result_type_member, $result_type);
378
                }
379
380
                return;
381
            }
382
383
            $from_loop_isset = (!($left_type_part instanceof TMixed) || $left_type_part->from_loop_isset)
384
                && (!($right_type_part instanceof TMixed) || $right_type_part->from_loop_isset);
385
386
            $result_type = Type::getMixed($from_loop_isset);
387
388
            return $result_type;
389
        }
390
391
        if ($statements_source && $codebase && $context) {
392
            if (!$context->collect_initializations
393
                && !$context->collect_mutations
394
                && $statements_source->getFilePath() === $statements_source->getRootFilePath()
395
                && (!(($parent_source = $statements_source->getSource())
396
                        instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
397
                    || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
398
            ) {
399
                $codebase->analyzer->incrementNonMixedCount($statements_source->getFilePath());
400
            }
401
        }
402
403
        if ($left_type_part instanceof TArray
404
            || $right_type_part instanceof TArray
405
            || $left_type_part instanceof ObjectLike
406
            || $right_type_part instanceof ObjectLike
407
            || $left_type_part instanceof TList
408
            || $right_type_part instanceof TList
409
        ) {
410
            if ((!$right_type_part instanceof TArray
411
                    && !$right_type_part instanceof ObjectLike
412
                    && !$right_type_part instanceof TList)
413
                || (!$left_type_part instanceof TArray
414
                    && !$left_type_part instanceof ObjectLike
415
                    && !$left_type_part instanceof TList)
416
            ) {
417
                if (!$left_type_part instanceof TArray
418
                    && !$left_type_part instanceof ObjectLike
419
                    && !$left_type_part instanceof TList
420
                ) {
421
                    $invalid_left_messages[] = 'Cannot add an array to a non-array ' . $left_type_part;
422
                } else {
423
                    $invalid_right_messages[] = 'Cannot add an array to a non-array ' . $right_type_part;
424
                }
425
426
                if ($left_type_part instanceof TArray
427
                    || $left_type_part instanceof ObjectLike
428
                    || $left_type_part instanceof TList
429
                ) {
430
                    $has_valid_left_operand = true;
431
                } elseif ($right_type_part instanceof TArray
432
                    || $right_type_part instanceof ObjectLike
433
                    || $right_type_part instanceof TList
434
                ) {
435
                    $has_valid_right_operand = true;
436
                }
437
438
                $result_type = Type::getArray();
439
440
                return;
441
            }
442
443
            $has_valid_right_operand = true;
444
            $has_valid_left_operand = true;
445
446
            if ($left_type_part instanceof ObjectLike
447
                && $right_type_part instanceof ObjectLike
448
            ) {
449
                $definitely_existing_mixed_right_properties = array_diff_key(
450
                    $right_type_part->properties,
451
                    $left_type_part->properties
452
                );
453
454
                $properties = $left_type_part->properties;
455
456
                foreach ($right_type_part->properties as $key => $type) {
457
                    if (!isset($properties[$key])) {
458
                        $properties[$key] = $type;
459
                    } elseif ($properties[$key]->possibly_undefined) {
460
                        $properties[$key] = Type::combineUnionTypes(
461
                            $properties[$key],
462
                            $type,
463
                            $codebase
464
                        );
465
466
                        $properties[$key]->possibly_undefined = $type->possibly_undefined;
467
                    }
468
                }
469
470
                if (!$left_type_part->sealed) {
471
                    foreach ($definitely_existing_mixed_right_properties as $key => $type) {
472
                        $properties[$key] = Type::combineUnionTypes(Type::getMixed(), $type);
473
                    }
474
                }
475
476
                $result_type_member = new Type\Union([new ObjectLike($properties)]);
477
            } else {
478
                $result_type_member = TypeCombination::combineTypes(
479
                    [$left_type_part, $right_type_part],
480
                    $codebase,
481
                    true
482
                );
483
            }
484
485
            if (!$result_type) {
486
                $result_type = $result_type_member;
487
            } else {
488
                $result_type = Type::combineUnionTypes($result_type_member, $result_type, $codebase, true);
489
            }
490
491
            if ($left instanceof PhpParser\Node\Expr\ArrayDimFetch
492
                && $context
493
                && $statements_source instanceof StatementsAnalyzer
494
            ) {
495
                ArrayAssignmentAnalyzer::updateArrayType(
496
                    $statements_source,
497
                    $left,
498
                    $right,
499
                    $result_type,
500
                    $context
501
                );
502
            }
503
504
            return;
505
        }
506
507
        if (($left_type_part instanceof TNamedObject && strtolower($left_type_part->value) === 'gmp')
508
            || ($right_type_part instanceof TNamedObject && strtolower($right_type_part->value) === 'gmp')
509
        ) {
510
            if ((($left_type_part instanceof TNamedObject
511
                        && strtolower($left_type_part->value) === 'gmp')
512
                    && (($right_type_part instanceof TNamedObject
513
                            && strtolower($right_type_part->value) === 'gmp')
514
                        || ($right_type_part->isNumericType() || $right_type_part instanceof TMixed)))
515
                || (($right_type_part instanceof TNamedObject
516
                        && strtolower($right_type_part->value) === 'gmp')
517
                    && (($left_type_part instanceof TNamedObject
518
                            && strtolower($left_type_part->value) === 'gmp')
519
                        || ($left_type_part->isNumericType() || $left_type_part instanceof TMixed)))
520
            ) {
521
                if (!$result_type) {
522
                    $result_type = new Type\Union([new TNamedObject('GMP')]);
523
                } else {
524
                    $result_type = Type::combineUnionTypes(
525
                        new Type\Union([new TNamedObject('GMP')]),
526
                        $result_type
527
                    );
528
                }
529
            } else {
530
                if ($statements_source && IssueBuffer::accepts(
531
                    new InvalidOperand(
532
                        'Cannot add GMP to non-numeric type',
533
                        new CodeLocation($statements_source, $parent)
534
                    ),
535
                    $statements_source->getSuppressedIssues()
536
                )) {
537
                    // fall through
538
                }
539
            }
540
541
            return;
542
        }
543
544
        if ($left_type_part->isNumericType() || $right_type_part->isNumericType()) {
545
            if (($left_type_part instanceof TNumeric || $right_type_part instanceof TNumeric)
546
                && ($left_type_part->isNumericType() && $right_type_part->isNumericType())
547
            ) {
548
                if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Mod) {
549
                    $result_type = Type::getInt();
550
                } elseif (!$result_type) {
551
                    $result_type = Type::getNumeric();
552
                } else {
553
                    $result_type = Type::combineUnionTypes(Type::getNumeric(), $result_type);
554
                }
555
556
                $has_valid_right_operand = true;
557
                $has_valid_left_operand = true;
558
559
                return;
560
            }
561
562
            if ($left_type_part instanceof TInt && $right_type_part instanceof TInt) {
563
                if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Mod) {
564
                    $result_type = Type::getInt();
565
                } elseif (!$result_type) {
566
                    $result_type = Type::getInt(true);
567
                } else {
568
                    $result_type = Type::combineUnionTypes(Type::getInt(true), $result_type);
569
                }
570
571
                $has_valid_right_operand = true;
572
                $has_valid_left_operand = true;
573
574
                return;
575
            }
576
577
            if ($left_type_part instanceof TFloat && $right_type_part instanceof TFloat) {
578
                if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Mod) {
579
                    $result_type = Type::getInt();
580
                } elseif (!$result_type) {
581
                    $result_type = Type::getFloat();
582
                } else {
583
                    $result_type = Type::combineUnionTypes(Type::getFloat(), $result_type);
584
                }
585
586
                $has_valid_right_operand = true;
587
                $has_valid_left_operand = true;
588
589
                return;
590
            }
591
592
            if (($left_type_part instanceof TFloat && $right_type_part instanceof TInt)
593
                || ($left_type_part instanceof TInt && $right_type_part instanceof TFloat)
594
            ) {
595
                if ($config->strict_binary_operands) {
596
                    if ($statements_source && IssueBuffer::accepts(
597
                        new InvalidOperand(
598
                            'Cannot add ints to floats',
599
                            new CodeLocation($statements_source, $parent)
600
                        ),
601
                        $statements_source->getSuppressedIssues()
602
                    )) {
603
                        // fall through
604
                    }
605
                }
606
607
                if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Mod) {
608
                    $result_type = Type::getInt();
609
                } elseif (!$result_type) {
610
                    $result_type = Type::getFloat();
611
                } else {
612
                    $result_type = Type::combineUnionTypes(Type::getFloat(), $result_type);
613
                }
614
615
                $has_valid_right_operand = true;
616
                $has_valid_left_operand = true;
617
618
                return;
619
            }
620
621
            if ($left_type_part->isNumericType() && $right_type_part->isNumericType()) {
622
                if ($config->strict_binary_operands) {
623
                    if ($statements_source && IssueBuffer::accepts(
624
                        new InvalidOperand(
625
                            'Cannot add numeric types together, please cast explicitly',
626
                            new CodeLocation($statements_source, $parent)
627
                        ),
628
                        $statements_source->getSuppressedIssues()
629
                    )) {
630
                        // fall through
631
                    }
632
                }
633
634
                if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Mod) {
635
                    $result_type = Type::getInt();
636
                } elseif (!$result_type) {
637
                    $result_type = Type::getFloat();
638
                } else {
639
                    $result_type = Type::combineUnionTypes(Type::getFloat(), $result_type);
640
                }
641
642
                $has_valid_right_operand = true;
643
                $has_valid_left_operand = true;
644
645
                return;
646
            }
647
648
            if (!$left_type_part->isNumericType()) {
649
                $invalid_left_messages[] = 'Cannot perform a numeric operation with a non-numeric type '
650
                    . $left_type_part;
651
                $has_valid_right_operand = true;
652
            } else {
653
                $invalid_right_messages[] = 'Cannot perform a numeric operation with a non-numeric type '
654
                    . $right_type_part;
655
                $has_valid_left_operand = true;
656
            }
657
        } else {
658
            $invalid_left_messages[] =
659
                'Cannot perform a numeric operation with non-numeric types ' . $left_type_part
660
                    . ' and ' . $right_type_part;
661
        }
662
    }
663
}
664