NonDivArithmeticOpAnalyzer::analyze()   F
last analyzed

Complexity

Conditions 48
Paths > 20000

Size

Total Lines 218

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 48
nc 36774
nop 7
dl 0
loc 218
rs 0
c 0
b 0
f 0

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
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