ConcatAnalyzer   F
last analyzed

Complexity

Total Complexity 121

Size/Duplication

Total Lines 467
Duplicated Lines 56.96 %

Coupling/Cohesion

Components 0
Dependencies 30

Importance

Changes 0
Metric Value
dl 266
loc 467
rs 2
c 0
b 0
f 0
wmc 121
lcom 0
cbo 30

1 Method

Rating   Name   Duplication   Size   Complexity  
F analyze() 266 456 121

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ConcatAnalyzer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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

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

1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\Expression\BinaryOp;
3
4
use PhpParser;
5
use Psalm\Internal\Analyzer\StatementsAnalyzer;
6
use Psalm\Internal\Analyzer\TypeAnalyzer;
7
use Psalm\CodeLocation;
8
use Psalm\Config;
9
use Psalm\Context;
10
use Psalm\Issue\FalseOperand;
11
use Psalm\Issue\ImplicitToStringCast;
12
use Psalm\Issue\ImpureMethodCall;
13
use Psalm\Issue\InvalidOperand;
14
use Psalm\Issue\MixedOperand;
15
use Psalm\Issue\NullOperand;
16
use Psalm\Issue\PossiblyFalseOperand;
17
use Psalm\Issue\PossiblyInvalidOperand;
18
use Psalm\Issue\PossiblyNullOperand;
19
use Psalm\IssueBuffer;
20
use Psalm\Type;
21
use Psalm\Type\Atomic\TNamedObject;
22
use function strtolower;
23
use function strlen;
24
25
/**
26
 * @internal
27
 */
28
class ConcatAnalyzer
29
{
30
    /**
31
     * @param  StatementsAnalyzer     $statements_analyzer
32
     * @param  PhpParser\Node\Expr   $left
33
     * @param  PhpParser\Node\Expr   $right
34
     * @param  Type\Union|null       &$result_type
35
     *
36
     * @return void
37
     */
38
    public static function analyze(
39
        StatementsAnalyzer $statements_analyzer,
40
        PhpParser\Node\Expr $left,
41
        PhpParser\Node\Expr $right,
42
        Context $context,
43
        Type\Union &$result_type = null
44
    ) {
45
        $codebase = $statements_analyzer->getCodebase();
46
47
        $left_type = $statements_analyzer->node_data->getType($left);
48
        $right_type = $statements_analyzer->node_data->getType($right);
49
        $config = Config::getInstance();
50
51
        if ($left_type && $right_type) {
52
            $result_type = Type::getString();
53
54
            if ($left_type->hasMixed() || $right_type->hasMixed()) {
55 View Code Duplication
                if (!$context->collect_initializations
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...
56
                    && !$context->collect_mutations
57
                    && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
58
                    && (!(($parent_source = $statements_analyzer->getSource())
59
                            instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
60
                        || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
61
                ) {
62
                    $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
63
                }
64
65
                if ($left_type->hasMixed()) {
66
                    if (IssueBuffer::accepts(
67
                        new MixedOperand(
68
                            'Left operand cannot be mixed',
69
                            new CodeLocation($statements_analyzer->getSource(), $left)
70
                        ),
71
                        $statements_analyzer->getSuppressedIssues()
72
                    )) {
73
                        // fall through
74
                    }
75
                } else {
76
                    if (IssueBuffer::accepts(
77
                        new MixedOperand(
78
                            'Right operand cannot be mixed',
79
                            new CodeLocation($statements_analyzer->getSource(), $right)
80
                        ),
81
                        $statements_analyzer->getSuppressedIssues()
82
                    )) {
83
                        // fall through
84
                    }
85
                }
86
87
                return;
88
            }
89
90 View Code Duplication
            if (!$context->collect_initializations
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...
91
                && !$context->collect_mutations
92
                && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
93
                && (!(($parent_source = $statements_analyzer->getSource())
94
                        instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
95
                    || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
96
            ) {
97
                $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
98
            }
99
100
            if ($left_type->isNull()) {
101
                if (IssueBuffer::accepts(
102
                    new NullOperand(
103
                        'Cannot concatenate with a ' . $left_type,
104
                        new CodeLocation($statements_analyzer->getSource(), $left)
105
                    ),
106
                    $statements_analyzer->getSuppressedIssues()
107
                )) {
108
                    // fall through
109
                }
110
111
                return;
112
            }
113
114
            if ($right_type->isNull()) {
115
                if (IssueBuffer::accepts(
116
                    new NullOperand(
117
                        'Cannot concatenate with a ' . $right_type,
118
                        new CodeLocation($statements_analyzer->getSource(), $right)
119
                    ),
120
                    $statements_analyzer->getSuppressedIssues()
121
                )) {
122
                    // fall through
123
                }
124
125
                return;
126
            }
127
128 View Code Duplication
            if ($left_type->isFalse()) {
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...
129
                if (IssueBuffer::accepts(
130
                    new FalseOperand(
131
                        'Cannot concatenate with a ' . $left_type,
132
                        new CodeLocation($statements_analyzer->getSource(), $left)
133
                    ),
134
                    $statements_analyzer->getSuppressedIssues()
135
                )) {
136
                    // fall through
137
                }
138
139
                return;
140
            }
141
142 View Code Duplication
            if ($right_type->isFalse()) {
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...
143
                if (IssueBuffer::accepts(
144
                    new FalseOperand(
145
                        'Cannot concatenate with a ' . $right_type,
146
                        new CodeLocation($statements_analyzer->getSource(), $right)
147
                    ),
148
                    $statements_analyzer->getSuppressedIssues()
149
                )) {
150
                    // fall through
151
                }
152
153
                return;
154
            }
155
156
            if ($left_type->isNullable() && !$left_type->ignore_nullable_issues) {
157
                if (IssueBuffer::accepts(
158
                    new PossiblyNullOperand(
159
                        'Cannot concatenate with a possibly null ' . $left_type,
160
                        new CodeLocation($statements_analyzer->getSource(), $left)
161
                    ),
162
                    $statements_analyzer->getSuppressedIssues()
163
                )) {
164
                    // fall through
165
                }
166
            }
167
168
            if ($right_type->isNullable() && !$right_type->ignore_nullable_issues) {
169
                if (IssueBuffer::accepts(
170
                    new PossiblyNullOperand(
171
                        'Cannot concatenate with a possibly null ' . $right_type,
172
                        new CodeLocation($statements_analyzer->getSource(), $right)
173
                    ),
174
                    $statements_analyzer->getSuppressedIssues()
175
                )) {
176
                    // fall through
177
                }
178
            }
179
180
            if ($left_type->isFalsable() && !$left_type->ignore_falsable_issues) {
181
                if (IssueBuffer::accepts(
182
                    new PossiblyFalseOperand(
183
                        'Cannot concatenate with a possibly false ' . $left_type,
184
                        new CodeLocation($statements_analyzer->getSource(), $left)
185
                    ),
186
                    $statements_analyzer->getSuppressedIssues()
187
                )) {
188
                    // fall through
189
                }
190
            }
191
192
            if ($right_type->isFalsable() && !$right_type->ignore_falsable_issues) {
193
                if (IssueBuffer::accepts(
194
                    new PossiblyFalseOperand(
195
                        'Cannot concatenate with a possibly false ' . $right_type,
196
                        new CodeLocation($statements_analyzer->getSource(), $right)
197
                    ),
198
                    $statements_analyzer->getSuppressedIssues()
199
                )) {
200
                    // fall through
201
                }
202
            }
203
204
            $left_type_match = true;
205
            $right_type_match = true;
206
207
            $has_valid_left_operand = false;
208
            $has_valid_right_operand = false;
209
210
            $left_comparison_result = new \Psalm\Internal\Analyzer\TypeComparisonResult();
211
            $right_comparison_result = new \Psalm\Internal\Analyzer\TypeComparisonResult();
212
213 View Code Duplication
            foreach ($left_type->getAtomicTypes() as $left_type_part) {
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...
214
                if ($left_type_part instanceof Type\Atomic\TTemplateParam) {
215
                    if (IssueBuffer::accepts(
216
                        new MixedOperand(
217
                            'Left operand cannot be mixed',
218
                            new CodeLocation($statements_analyzer->getSource(), $left)
219
                        ),
220
                        $statements_analyzer->getSuppressedIssues()
221
                    )) {
222
                        // fall through
223
                    }
224
225
                    return;
226
                }
227
228
                if ($left_type_part instanceof Type\Atomic\TNull || $left_type_part instanceof Type\Atomic\TFalse) {
229
                    continue;
230
                }
231
232
                $left_type_part_match = TypeAnalyzer::isAtomicContainedBy(
233
                    $codebase,
234
                    $left_type_part,
235
                    new Type\Atomic\TString,
236
                    false,
237
                    false,
238
                    $left_comparison_result
239
                );
240
241
                $left_type_match = $left_type_match && $left_type_part_match;
242
243
                $has_valid_left_operand = $has_valid_left_operand || $left_type_part_match;
244
245
                if ($left_comparison_result->to_string_cast && $config->strict_binary_operands) {
246
                    if (IssueBuffer::accepts(
247
                        new ImplicitToStringCast(
248
                            'Left side of concat op expects string, '
249
                                . '\'' . $left_type . '\' provided with a __toString method',
250
                            new CodeLocation($statements_analyzer->getSource(), $left)
251
                        ),
252
                        $statements_analyzer->getSuppressedIssues()
253
                    )) {
254
                        // fall through
255
                    }
256
                }
257
258
                foreach ($left_type->getAtomicTypes() as $atomic_type) {
259
                    if ($atomic_type instanceof TNamedObject) {
260
                        $to_string_method_id = new \Psalm\Internal\MethodIdentifier(
261
                            $atomic_type->value,
262
                            '__tostring'
263
                        );
264
265
                        if ($codebase->methods->methodExists(
266
                            $to_string_method_id,
267
                            $context->calling_method_id,
268
                            $codebase->collect_locations
269
                                ? new CodeLocation($statements_analyzer->getSource(), $left)
270
                                : null,
271
                            !$context->collect_initializations
272
                                && !$context->collect_mutations
273
                                ? $statements_analyzer
274
                                : null,
275
                            $statements_analyzer->getFilePath()
276
                        )) {
277
                            try {
278
                                $storage = $codebase->methods->getStorage($to_string_method_id);
279
                            } catch (\UnexpectedValueException $e) {
280
                                continue;
281
                            }
282
283
                            if ($context->mutation_free && !$storage->mutation_free) {
284
                                if (IssueBuffer::accepts(
285
                                    new ImpureMethodCall(
286
                                        'Cannot call a possibly-mutating method '
287
                                            . $atomic_type->value . '::__toString from a pure context',
288
                                        new CodeLocation($statements_analyzer, $left)
289
                                    ),
290
                                    $statements_analyzer->getSuppressedIssues()
291
                                )) {
292
                                    // fall through
293
                                }
294
                            }
295
                        }
296
                    }
297
                }
298
            }
299
300 View Code Duplication
            foreach ($right_type->getAtomicTypes() as $right_type_part) {
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...
301
                if ($right_type_part instanceof Type\Atomic\TTemplateParam) {
302
                    if (IssueBuffer::accepts(
303
                        new MixedOperand(
304
                            'Right operand cannot be a template param',
305
                            new CodeLocation($statements_analyzer->getSource(), $right)
306
                        ),
307
                        $statements_analyzer->getSuppressedIssues()
308
                    )) {
309
                        // fall through
310
                    }
311
312
                    return;
313
                }
314
315
                if ($right_type_part instanceof Type\Atomic\TNull || $right_type_part instanceof Type\Atomic\TFalse) {
316
                    continue;
317
                }
318
319
                $right_type_part_match = TypeAnalyzer::isAtomicContainedBy(
320
                    $codebase,
321
                    $right_type_part,
322
                    new Type\Atomic\TString,
323
                    false,
324
                    false,
325
                    $right_comparison_result
326
                );
327
328
                $right_type_match = $right_type_match && $right_type_part_match;
329
330
                $has_valid_right_operand = $has_valid_right_operand || $right_type_part_match;
331
332
                if ($right_comparison_result->to_string_cast && $config->strict_binary_operands) {
333
                    if (IssueBuffer::accepts(
334
                        new ImplicitToStringCast(
335
                            'Right side of concat op expects string, '
336
                                . '\'' . $right_type . '\' provided with a __toString method',
337
                            new CodeLocation($statements_analyzer->getSource(), $right)
338
                        ),
339
                        $statements_analyzer->getSuppressedIssues()
340
                    )) {
341
                        // fall through
342
                    }
343
                }
344
345
                foreach ($right_type->getAtomicTypes() as $atomic_type) {
346
                    if ($atomic_type instanceof TNamedObject) {
347
                        $to_string_method_id = new \Psalm\Internal\MethodIdentifier(
348
                            $atomic_type->value,
349
                            '__tostring'
350
                        );
351
352
                        if ($codebase->methods->methodExists(
353
                            $to_string_method_id,
354
                            $context->calling_method_id,
355
                            $codebase->collect_locations
356
                                ? new CodeLocation($statements_analyzer->getSource(), $right)
357
                                : null,
358
                            !$context->collect_initializations
359
                                && !$context->collect_mutations
360
                                ? $statements_analyzer
361
                                : null,
362
                            $statements_analyzer->getFilePath()
363
                        )) {
364
                            try {
365
                                $storage = $codebase->methods->getStorage($to_string_method_id);
366
                            } catch (\UnexpectedValueException $e) {
367
                                continue;
368
                            }
369
370
                            if ($context->mutation_free && !$storage->mutation_free) {
371
                                if (IssueBuffer::accepts(
372
                                    new ImpureMethodCall(
373
                                        'Cannot call a possibly-mutating method '
374
                                            . $atomic_type->value . '::__toString from a pure context',
375
                                        new CodeLocation($statements_analyzer, $right)
376
                                    ),
377
                                    $statements_analyzer->getSuppressedIssues()
378
                                )) {
379
                                    // fall through
380
                                }
381
                            }
382
                        }
383
                    }
384
                }
385
            }
386
387 View Code Duplication
            if (!$left_type_match
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...
388
                && (!$left_comparison_result->scalar_type_match_found || $config->strict_binary_operands)
389
            ) {
390
                if ($has_valid_left_operand) {
391
                    if (IssueBuffer::accepts(
392
                        new PossiblyInvalidOperand(
393
                            'Cannot concatenate with a ' . $left_type,
394
                            new CodeLocation($statements_analyzer->getSource(), $left)
395
                        ),
396
                        $statements_analyzer->getSuppressedIssues()
397
                    )) {
398
                        // fall through
399
                    }
400
                } else {
401
                    if (IssueBuffer::accepts(
402
                        new InvalidOperand(
403
                            'Cannot concatenate with a ' . $left_type,
404
                            new CodeLocation($statements_analyzer->getSource(), $left)
405
                        ),
406
                        $statements_analyzer->getSuppressedIssues()
407
                    )) {
408
                        // fall through
409
                    }
410
                }
411
            }
412
413 View Code Duplication
            if (!$right_type_match
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...
414
                && (!$right_comparison_result->scalar_type_match_found || $config->strict_binary_operands)
415
            ) {
416
                if ($has_valid_right_operand) {
417
                    if (IssueBuffer::accepts(
418
                        new PossiblyInvalidOperand(
419
                            'Cannot concatenate with a ' . $right_type,
420
                            new CodeLocation($statements_analyzer->getSource(), $right)
421
                        ),
422
                        $statements_analyzer->getSuppressedIssues()
423
                    )) {
424
                        // fall through
425
                    }
426
                } else {
427
                    if (IssueBuffer::accepts(
428
                        new InvalidOperand(
429
                            'Cannot concatenate with a ' . $right_type,
430
                            new CodeLocation($statements_analyzer->getSource(), $right)
431
                        ),
432
                        $statements_analyzer->getSuppressedIssues()
433
                    )) {
434
                        // fall through
435
                    }
436
                }
437
            }
438
        }
439
440
        // When concatenating two known string literals (with only one possibility),
441
        // put the concatenated string into $result_type
442
        if ($left_type && $right_type && $left_type->isSingleStringLiteral() && $right_type->isSingleStringLiteral()) {
443
            $literal = $left_type->getSingleStringLiteral()->value . $right_type->getSingleStringLiteral()->value;
444
            if (strlen($literal) <= 1000) {
445
                // Limit these to 10000 bytes to avoid extremely large union types from repeated concatenations, etc
446
                $result_type = Type::getString($literal);
447
            }
448
        } else {
449
            if ($left_type
450
                && $right_type
451
            ) {
452
                $left_type_literal_value = $left_type->isSingleStringLiteral()
453
                    ? $left_type->getSingleStringLiteral()->value
454
                    : null;
455
456
                $right_type_literal_value = $right_type->isSingleStringLiteral()
457
                    ? $right_type->getSingleStringLiteral()->value
458
                    : null;
459
460
                if (($left_type->getId() === 'lowercase-string'
461
                        || $left_type->getId() === 'non-empty-lowercase-string'
462
                        || $left_type->isInt()
463
                        || ($left_type_literal_value !== null
464
                            && strtolower($left_type_literal_value) === $left_type_literal_value))
465
                    && ($right_type->getId() === 'lowercase-string'
466
                        || $right_type->getId() === 'non-empty-lowercase-string'
467
                        || $right_type->isInt()
468
                        || ($right_type_literal_value !== null
469
                            && strtolower($right_type_literal_value) === $right_type_literal_value))
470
                ) {
471
                    if ($left_type->getId() === 'non-empty-lowercase-string'
472
                        || $left_type->isInt()
473
                        || ($left_type_literal_value !== null
474
                            && strtolower($left_type_literal_value) === $left_type_literal_value)
475
                        || $right_type->getId() === 'non-empty-lowercase-string'
476
                        || $right_type->isInt()
477
                        || ($right_type_literal_value !== null
478
                            && strtolower($right_type_literal_value) === $right_type_literal_value)
479
                    ) {
480
                        $result_type = new Type\Union([new Type\Atomic\TNonEmptyLowercaseString()]);
481
                    } else {
482
                        $result_type = new Type\Union([new Type\Atomic\TLowercaseString()]);
483
                    }
484
                } elseif ($left_type->getId() === 'non-empty-string'
485
                    || $right_type->getId() === 'non-empty-string'
486
                    || $left_type_literal_value
487
                    || $right_type_literal_value
488
                ) {
489
                    $result_type = new Type\Union([new Type\Atomic\TNonEmptyString()]);
490
                }
491
            }
492
        }
493
    }
494
}
495