FileAnalyzer::clearSourceBeforeDestruction()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
namespace Psalm\Internal\Analyzer;
3
4
use PhpParser;
5
use Psalm\Codebase;
6
use Psalm\Context;
7
use Psalm\Exception\UnpreparedAnalysisException;
8
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
9
use Psalm\Issue\UncaughtThrowInGlobalScope;
10
use Psalm\IssueBuffer;
11
use Psalm\StatementsSource;
12
use Psalm\Type;
13
use function implode;
14
use function strtolower;
15
use function strpos;
16
use function array_keys;
17
use function count;
18
19
/**
20
 * @internal
21
 */
22
class FileAnalyzer extends SourceAnalyzer implements StatementsSource
23
{
24
    use CanAlias;
25
26
    /**
27
     * @var string
28
     */
29
    protected $file_name;
30
31
    /**
32
     * @var string
33
     */
34
    protected $file_path;
35
36
    /**
37
     * @var string|null
38
     */
39
    protected $root_file_path;
40
41
    /**
42
     * @var string|null
43
     */
44
    protected $root_file_name;
45
46
    /**
47
     * @var array<string, bool>
48
     */
49
    private $required_file_paths = [];
50
51
    /**
52
     * @var array<string, bool>
53
     */
54
    private $parent_file_paths = [];
55
56
    /**
57
     * @var array<string>
58
     */
59
    private $suppressed_issues = [];
60
61
    /**
62
     * @var array<string, array<string, string>>
63
     */
64
    private $namespace_aliased_classes = [];
65
66
    /**
67
     * @var array<string, array<string, string>>
68
     */
69
    private $namespace_aliased_classes_flipped = [];
70
71
    /**
72
     * @var array<string, array<string, string>>
73
     */
74
    private $namespace_aliased_classes_flipped_replaceable = [];
75
76
    /**
77
     * @var array<string, InterfaceAnalyzer>
78
     */
79
    public $interface_analyzers_to_analyze = [];
80
81
    /**
82
     * @var array<lowercase-string, ClassAnalyzer>
83
     */
84
    public $class_analyzers_to_analyze = [];
85
86
    /**
87
     * @var null|Context
88
     */
89
    public $context;
90
91
    /**
92
     * @var ProjectAnalyzer
93
     */
94
    public $project_analyzer;
95
96
    /**
97
     * @var Codebase
98
     */
99
    public $codebase;
100
101
    /**
102
     * @var int
103
     */
104
    private $first_statement_offset = -1;
105
106
    /** @var ?\Psalm\Internal\Provider\NodeDataProvider */
107
    private $node_data;
108
109
    /** @var ?Type\Union */
110
    private $return_type;
111
112
    /**
113
     * @param string  $file_path
114
     * @param string  $file_name
115
     */
116
    public function __construct(ProjectAnalyzer $project_analyzer, $file_path, $file_name)
117
    {
118
        $this->source = $this;
119
        $this->file_path = $file_path;
120
        $this->file_name = $file_name;
121
        $this->project_analyzer = $project_analyzer;
122
        $this->codebase = $project_analyzer->getCodebase();
123
    }
124
125
    /**
126
     * @param  bool $preserve_analyzers
127
     *
128
     * @return void
129
     */
130
    public function analyze(
131
        Context $file_context = null,
132
        $preserve_analyzers = false,
133
        Context $global_context = null
134
    ) {
135
        $codebase = $this->project_analyzer->getCodebase();
136
137
        $file_storage = $codebase->file_storage_provider->get($this->file_path);
138
139
        if (!$file_storage->deep_scan && !$codebase->server_mode) {
140
            throw new UnpreparedAnalysisException('File ' . $this->file_path . ' has not been properly scanned');
141
        }
142
143
        if ($file_storage->has_visitor_issues) {
144
            return;
145
        }
146
147
        if ($file_context) {
148
            $this->context = $file_context;
149
        }
150
151
        if (!$this->context) {
152
            $this->context = new Context();
153
        }
154
155
        if ($codebase->config->useStrictTypesForFile($this->file_path)) {
156
            $this->context->strict_types = true;
157
        }
158
159
        $this->context->is_global = true;
160
        $this->context->defineGlobals();
161
        $this->context->collect_exceptions = $codebase->config->check_for_throws_in_global_scope;
162
163
        try {
164
            $stmts = $codebase->getStatementsForFile($this->file_path);
165
        } catch (PhpParser\Error $e) {
166
            return;
167
        }
168
        foreach ($codebase->config->before_analyze_file as $plugin_class) {
169
            $plugin_class::beforeAnalyzeFile($this);
170
        }
171
172
        if ($codebase->alter_code) {
173
            foreach ($stmts as $stmt) {
174
                if (!$stmt instanceof PhpParser\Node\Stmt\Declare_) {
175
                    $this->first_statement_offset = (int) $stmt->getAttribute('startFilePos');
176
                    break;
177
                }
178
            }
179
        }
180
181
        $leftover_stmts = $this->populateCheckers($stmts);
182
183
        $this->node_data = new \Psalm\Internal\Provider\NodeDataProvider();
184
        $statements_analyzer = new StatementsAnalyzer($this, $this->node_data);
185
186
        foreach ($file_storage->docblock_issues as $docblock_issue) {
187
            IssueBuffer::add($docblock_issue);
188
        }
189
190
        // if there are any leftover statements, evaluate them,
191
        // in turn causing the classes/interfaces be evaluated
192
        if ($leftover_stmts) {
193
            $statements_analyzer->analyze($leftover_stmts, $this->context, $global_context, true);
194
195
            foreach ($leftover_stmts as $leftover_stmt) {
196
                if ($leftover_stmt instanceof PhpParser\Node\Stmt\Return_) {
197
                    if ($leftover_stmt->expr) {
198
                        $this->return_type = $statements_analyzer->node_data->getType($leftover_stmt->expr)
199
                            ?: Type::getMixed();
200
                    } else {
201
                        $this->return_type = Type::getVoid();
202
                    }
203
204
                    break;
205
                }
206
            }
207
        }
208
209
        // check any leftover interfaces not already evaluated
210
        foreach ($this->interface_analyzers_to_analyze as $interface_analyzer) {
211
            $interface_analyzer->analyze();
212
        }
213
214
        // check any leftover classes not already evaluated
215
216
        foreach ($this->class_analyzers_to_analyze as $class_analyzer) {
217
            $class_analyzer->analyze(null, $this->context);
218
        }
219
220
        if (!$preserve_analyzers) {
221
            $this->class_analyzers_to_analyze = [];
222
            $this->interface_analyzers_to_analyze = [];
223
        }
224
225
        if ($codebase->config->check_for_throws_in_global_scope) {
226
            $uncaught_throws = $statements_analyzer->getUncaughtThrows($this->context);
227
            foreach ($uncaught_throws as $possibly_thrown_exception => $codelocations) {
228
                foreach ($codelocations as $codelocation) {
229
                    // issues are suppressed in ThrowAnalyzer, CallAnalyzer, etc.
230
                    if (IssueBuffer::accepts(
231
                        new UncaughtThrowInGlobalScope(
232
                            $possibly_thrown_exception . ' is thrown but not caught in global scope',
233
                            $codelocation
234
                        )
235
                    )) {
236
                        // fall through
237
                    }
238
                }
239
            }
240
        }
241
    }
242
243
    /**
244
     * @param  array<int, PhpParser\Node\Stmt>  $stmts
245
     *
246
     * @return array<int, PhpParser\Node\Stmt>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
247
     */
248
    public function populateCheckers(array $stmts)
249
    {
250
        $leftover_stmts = [];
251
252
        foreach ($stmts as $stmt) {
253
            if ($stmt instanceof PhpParser\Node\Stmt\ClassLike) {
254
                $this->populateClassLikeAnalyzers($stmt);
255
            } elseif ($stmt instanceof PhpParser\Node\Stmt\Namespace_) {
256
                $namespace_name = $stmt->name ? implode('\\', $stmt->name->parts) : '';
257
258
                $namespace_analyzer = new NamespaceAnalyzer($stmt, $this);
259
                $namespace_analyzer->collectAnalyzableInformation();
260
261
                $this->namespace_aliased_classes[$namespace_name] = $namespace_analyzer->getAliases()->uses;
262
                $this->namespace_aliased_classes_flipped[$namespace_name] =
263
                    $namespace_analyzer->getAliasedClassesFlipped();
264
                $this->namespace_aliased_classes_flipped_replaceable[$namespace_name] =
265
                    $namespace_analyzer->getAliasedClassesFlippedReplaceable();
266
            } elseif ($stmt instanceof PhpParser\Node\Stmt\Use_) {
267
                $this->visitUse($stmt);
268
            } elseif ($stmt instanceof PhpParser\Node\Stmt\GroupUse) {
269
                $this->visitGroupUse($stmt);
270
            } else {
271
                if ($stmt instanceof PhpParser\Node\Stmt\If_) {
272
                    foreach ($stmt->stmts as $if_stmt) {
273
                        if ($if_stmt instanceof PhpParser\Node\Stmt\ClassLike) {
274
                            $this->populateClassLikeAnalyzers($if_stmt);
275
                        }
276
                    }
277
                }
278
279
                $leftover_stmts[] = $stmt;
280
            }
281
        }
282
283
        return $leftover_stmts;
284
    }
285
286
    /**
287
     * @return void
288
     */
289
    private function populateClassLikeAnalyzers(PhpParser\Node\Stmt\ClassLike $stmt)
290
    {
291
        if (!$stmt->name) {
292
            return;
293
        }
294
295
        // this can happen when stubbing
296
        if (!$this->codebase->classOrInterfaceExists($stmt->name->name)) {
297
            return;
298
        }
299
300
        if ($stmt instanceof PhpParser\Node\Stmt\Class_) {
301
            $class_analyzer = new ClassAnalyzer($stmt, $this, $stmt->name->name);
302
303
            $fq_class_name = $class_analyzer->getFQCLN();
304
305
            $this->class_analyzers_to_analyze[strtolower($fq_class_name)] = $class_analyzer;
306
        } elseif ($stmt instanceof PhpParser\Node\Stmt\Interface_) {
307
            $class_analyzer = new InterfaceAnalyzer($stmt, $this, $stmt->name->name);
308
309
            $fq_class_name = $class_analyzer->getFQCLN();
310
311
            $this->interface_analyzers_to_analyze[$fq_class_name] = $class_analyzer;
312
        }
313
    }
314
315
    /**
316
     * @param string       $fq_class_name
317
     * @param ClassAnalyzer $class_analyzer
318
     *
319
     * @return  void
320
     */
321
    public function addNamespacedClassAnalyzer($fq_class_name, ClassAnalyzer $class_analyzer)
322
    {
323
        $this->class_analyzers_to_analyze[strtolower($fq_class_name)] = $class_analyzer;
324
    }
325
326
    /**
327
     * @param string            $fq_class_name
328
     * @param InterfaceAnalyzer  $interface_analyzer
329
     *
330
     * @return  void
331
     */
332
    public function addNamespacedInterfaceAnalyzer($fq_class_name, InterfaceAnalyzer $interface_analyzer)
333
    {
334
        $this->interface_analyzers_to_analyze[strtolower($fq_class_name)] = $interface_analyzer;
335
    }
336
337
    /**
338
     * @return void
339
     */
340
    public function getMethodMutations(
341
        \Psalm\Internal\MethodIdentifier $method_id,
342
        Context $this_context,
343
        bool $from_project_analyzer = false
344
    ) {
345
        $fq_class_name = $method_id->fq_class_name;
346
        $method_name = $method_id->method_name;
347
        $fq_class_name_lc = strtolower($fq_class_name);
348
349
        if (isset($this->class_analyzers_to_analyze[$fq_class_name_lc])) {
350
            $class_analyzer_to_examine = $this->class_analyzers_to_analyze[$fq_class_name_lc];
351
        } else {
352
            if (!$from_project_analyzer) {
353
                $this->project_analyzer->getMethodMutations(
354
                    $method_id,
355
                    $this_context,
356
                    $this->getRootFilePath(),
357
                    $this->getRootFileName()
358
                );
359
            }
360
361
            return;
362
        }
363
364
        $call_context = new Context($this_context->self);
365
        $call_context->collect_mutations = $this_context->collect_mutations;
366
        $call_context->collect_initializations = $this_context->collect_initializations;
367
        $call_context->collect_nonprivate_initializations = $this_context->collect_nonprivate_initializations;
368
        $call_context->initialized_methods = $this_context->initialized_methods;
369
        $call_context->include_location = $this_context->include_location;
370
        $call_context->calling_method_id = $this_context->calling_method_id;
371
372
        foreach ($this_context->vars_possibly_in_scope as $var => $_) {
373
            if (strpos($var, '$this->') === 0) {
374
                $call_context->vars_possibly_in_scope[$var] = true;
375
            }
376
        }
377
378
        foreach ($this_context->vars_in_scope as $var => $type) {
379
            if (strpos($var, '$this->') === 0) {
380
                $call_context->vars_in_scope[$var] = $type;
381
            }
382
        }
383
384
        if (!isset($this_context->vars_in_scope['$this'])) {
385
            throw new \UnexpectedValueException('Should exist');
386
        }
387
388
        $call_context->vars_in_scope['$this'] = $this_context->vars_in_scope['$this'];
389
390
        $class_analyzer_to_examine->getMethodMutations($method_name, $call_context);
391
392
        foreach ($call_context->vars_possibly_in_scope as $var => $_) {
393
            $this_context->vars_possibly_in_scope[$var] = true;
394
        }
395
396
        foreach ($call_context->vars_in_scope as $var => $type) {
397
            $this_context->vars_in_scope[$var] = $type;
398
        }
399
    }
400
401
    public function getFunctionLikeAnalyzer(\Psalm\Internal\MethodIdentifier $method_id) : ?FunctionLikeAnalyzer
402
    {
403
        $fq_class_name = $method_id->fq_class_name;
404
        $method_name = $method_id->method_name;
405
406
        $fq_class_name_lc = strtolower($fq_class_name);
407
408
        if (!isset($this->class_analyzers_to_analyze[$fq_class_name_lc])) {
409
            return null;
410
        }
411
412
        $class_analyzer_to_examine = $this->class_analyzers_to_analyze[$fq_class_name_lc];
413
414
        return $class_analyzer_to_examine->getFunctionLikeAnalyzer($method_name);
415
    }
416
417
    /**
418
     * @return null|string
419
     */
420
    public function getNamespace()
421
    {
422
        return null;
423
    }
424
425
    /**
426
     * @param  string|null $namespace_name
427
     *
428
     * @return array<string, string>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
429
     */
430
    public function getAliasedClassesFlipped($namespace_name = null)
431
    {
432
        if ($namespace_name && isset($this->namespace_aliased_classes_flipped[$namespace_name])) {
433
            return $this->namespace_aliased_classes_flipped[$namespace_name];
434
        }
435
436
        return $this->aliased_classes_flipped;
437
    }
438
439
    /**
440
     * @param  string|null $namespace_name
441
     *
442
     * @return array<string, string>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
443
     */
444
    public function getAliasedClassesFlippedReplaceable($namespace_name = null)
445
    {
446
        if ($namespace_name && isset($this->namespace_aliased_classes_flipped_replaceable[$namespace_name])) {
447
            return $this->namespace_aliased_classes_flipped_replaceable[$namespace_name];
448
        }
449
450
        return $this->aliased_classes_flipped_replaceable;
451
    }
452
453
    /**
454
     * @return void
455
     */
456
    public static function clearCache()
457
    {
458
        \Psalm\Internal\Type\TypeTokenizer::clearCache();
459
        \Psalm\Internal\Codebase\Reflection::clearCache();
460
        \Psalm\Internal\Codebase\Functions::clearCache();
461
        IssueBuffer::clearCache();
462
        FileManipulationBuffer::clearCache();
463
        FunctionLikeAnalyzer::clearCache();
464
        \Psalm\Internal\Provider\ClassLikeStorageProvider::deleteAll();
465
        \Psalm\Internal\Provider\FileStorageProvider::deleteAll();
466
        \Psalm\Internal\Provider\FileReferenceProvider::clearCache();
467
    }
468
469
    /**
470
     * @return string
471
     */
472
    public function getFileName()
473
    {
474
        return $this->file_name;
475
    }
476
477
    /**
478
     * @return string
479
     */
480
    public function getFilePath()
481
    {
482
        return $this->file_path;
483
    }
484
485
    /**
486
     * @return string
487
     */
488
    public function getRootFileName()
489
    {
490
        return $this->root_file_name ?: $this->file_name;
491
    }
492
493
    /**
494
     * @return string
495
     */
496
    public function getRootFilePath()
497
    {
498
        return $this->root_file_path ?: $this->file_path;
499
    }
500
501
    /**
502
     * @param string $file_path
503
     * @param string $file_name
504
     *
505
     * @return void
506
     */
507
    public function setRootFilePath($file_path, $file_name)
508
    {
509
        $this->root_file_name = $file_name;
510
        $this->root_file_path = $file_path;
511
    }
512
513
    /**
514
     * @param string $file_path
515
     *
516
     * @return void
517
     */
518
    public function addRequiredFilePath($file_path)
519
    {
520
        $this->required_file_paths[$file_path] = true;
521
    }
522
523
    /**
524
     * @param string $file_path
525
     *
526
     * @return void
527
     */
528
    public function addParentFilePath($file_path)
529
    {
530
        $this->parent_file_paths[$file_path] = true;
531
    }
532
533
    /**
534
     * @param string $file_path
535
     *
536
     * @return bool
537
     */
538
    public function hasParentFilePath($file_path)
539
    {
540
        return $this->file_path === $file_path || isset($this->parent_file_paths[$file_path]);
541
    }
542
543
    /**
544
     * @param string $file_path
545
     *
546
     * @return bool
547
     */
548
    public function hasAlreadyRequiredFilePath($file_path)
549
    {
550
        return isset($this->required_file_paths[$file_path]);
551
    }
552
553
    /**
554
     * @return array<int, string>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
555
     */
556
    public function getRequiredFilePaths()
557
    {
558
        return array_keys($this->required_file_paths);
559
    }
560
561
    /**
562
     * @return array<int, string>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
563
     */
564
    public function getParentFilePaths()
565
    {
566
        return array_keys($this->parent_file_paths);
567
    }
568
569
    /**
570
     * @return int
571
     */
572
    public function getRequireNesting()
573
    {
574
        return count($this->parent_file_paths);
575
    }
576
577
    /**
578
     * @return array<string>
579
     */
580
    public function getSuppressedIssues()
581
    {
582
        return $this->suppressed_issues;
583
    }
584
585
    /**
586
     * @param array<int, string> $new_issues
587
     *
588
     * @return void
589
     */
590
    public function addSuppressedIssues(array $new_issues)
591
    {
592
        if (isset($new_issues[0])) {
593
            $new_issues = \array_combine($new_issues, $new_issues);
594
        }
595
596
        $this->suppressed_issues = $new_issues + $this->suppressed_issues;
597
    }
598
599
    /**
600
     * @param array<int, string> $new_issues
601
     *
602
     * @return void
603
     */
604
    public function removeSuppressedIssues(array $new_issues)
605
    {
606
        if (isset($new_issues[0])) {
607
            $new_issues = \array_combine($new_issues, $new_issues);
608
        }
609
610
        $this->suppressed_issues = \array_diff_key($this->suppressed_issues, $new_issues);
611
    }
612
613
    /**
614
     * @return null|string
615
     */
616
    public function getFQCLN()
617
    {
618
        return null;
619
    }
620
621
    /**
622
     * @return null|string
623
     */
624
    public function getParentFQCLN()
625
    {
626
        return null;
627
    }
628
629
    /**
630
     * @return null|string
631
     */
632
    public function getClassName()
633
    {
634
        return null;
635
    }
636
637
    /**
638
     * @return array<string, array<string, array{Type\Union}>>|null
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
639
     */
640
    public function getTemplateTypeMap()
641
    {
642
        return null;
643
    }
644
645
    /**
646
     * @return bool
647
     */
648
    public function isStatic()
649
    {
650
        return false;
651
    }
652
653
    public function getFileAnalyzer() : FileAnalyzer
654
    {
655
        return $this;
656
    }
657
658
    public function getProjectAnalyzer() : ProjectAnalyzer
659
    {
660
        return $this->project_analyzer;
661
    }
662
663
    public function getCodebase() : Codebase
664
    {
665
        return $this->codebase;
666
    }
667
668
    public function getFirstStatementOffset() : int
669
    {
670
        return $this->first_statement_offset;
671
    }
672
673
    public function getNodeTypeProvider() : \Psalm\NodeTypeProvider
674
    {
675
        if (!$this->node_data) {
676
            throw new \UnexpectedValueException('There should be a node type provider');
677
        }
678
679
        return $this->node_data;
680
    }
681
682
    public function getReturnType() : ?Type\Union
683
    {
684
        return $this->return_type;
685
    }
686
687
    public function clearSourceBeforeDestruction() : void
688
    {
689
        /** @psalm-suppress PossiblyNullPropertyAssignmentValue */
690
        $this->source = null;
691
    }
692
}
693