Test Setup Failed
Push — master ( 8fbc8d...aaba3a )
by Matthew
05:34
created

FileFilter::allowsVariable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
namespace Psalm\Config;
3
4
use function array_filter;
5
use function array_map;
6
use const DIRECTORY_SEPARATOR;
7
use const E_WARNING;
8
use function explode;
9
use function glob;
10
use function in_array;
11
use function is_bool;
12
use function is_dir;
13
use function preg_match;
14
use function preg_replace;
15
use Psalm\Exception\ConfigException;
16
use function readlink;
17
use function realpath;
18
use function restore_error_handler;
19
use function set_error_handler;
20
use SimpleXMLElement;
21
use function str_replace;
22
use function stripos;
23
use function strpos;
24
use function strtolower;
25
use const GLOB_NOSORT;
26
use const GLOB_ONLYDIR;
27
28
class FileFilter
29
{
30
    /**
31
     * @var array<string>
32
     */
33
    protected $directories = [];
34
35
    /**
36
     * @var array<string>
37
     */
38
    protected $files = [];
39
40
    /**
41
     * @var array<string>
42
     */
43
    protected $fq_classlike_names = [];
44
45
    /**
46
     * @var array<string>
47
     */
48
    protected $fq_classlike_patterns = [];
49
50
    /**
51
     * @var array<string>
52
     */
53
    protected $method_ids = [];
54
55
    /**
56
     * @var array<string>
57
     */
58
    protected $property_ids = [];
59
60
    /**
61
     * @var array<string>
62
     */
63
    protected $var_names = [];
64
65
    /**
66
     * @var array<string>
67
     */
68
    protected $files_lowercase = [];
69
70
    /**
71
     * @var bool
72
     */
73
    protected $inclusive;
74
75
    /**
76
     * @var array<string, bool>
77
     */
78
    protected $ignore_type_stats = [];
79
80
    /**
81
     * @var array<string, bool>
82
     */
83
    protected $declare_strict_types = [];
84
85
    public function __construct(bool $inclusive)
86
    {
87
        $this->inclusive = $inclusive;
88
    }
89
90
    /**
91
     * @param  SimpleXMLElement $e
92
     * @param  string           $base_dir
93
     * @param  bool             $inclusive
94
     *
95
     * @return static
96
     */
97
    public static function loadFromXMLElement(
98
        SimpleXMLElement $e,
99
        $base_dir,
100
        $inclusive
101
    ) {
102
        $allow_missing_files = ((string) $e['allowMissingFiles']) === 'true';
103
104
        $filter = new static($inclusive);
105
106
        if ($e->directory) {
107
            /** @var \SimpleXMLElement $directory */
108
            foreach ($e->directory as $directory) {
109
                $directory_path = (string) $directory['name'];
110
                $ignore_type_stats = strtolower(
111
                    isset($directory['ignoreTypeStats']) ? (string) $directory['ignoreTypeStats'] : ''
112
                ) === 'true';
113
                $declare_strict_types = strtolower(
114
                    isset($directory['useStrictTypes']) ? (string) $directory['useStrictTypes'] : ''
115
                ) === 'true';
116
117
                if ($directory_path[0] === '/' && DIRECTORY_SEPARATOR === '/') {
118
                    $prospective_directory_path = $directory_path;
119
                } else {
120
                    $prospective_directory_path = $base_dir . DIRECTORY_SEPARATOR . $directory_path;
121
                }
122
123
                if (strpos($prospective_directory_path, '*') !== false) {
124
                    $globs = array_map(
125
                        'realpath',
126
                        glob($prospective_directory_path, GLOB_ONLYDIR)
127
                    );
128
129 View Code Duplication
                    if (empty($globs)) {
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...
130
                        if ($allow_missing_files) {
131
                            continue;
132
                        }
133
134
                        throw new ConfigException(
135
                            'Could not resolve config path to ' . $base_dir
136
                                . DIRECTORY_SEPARATOR . (string)$directory['name']
137
                        );
138
                    }
139
140
                    foreach ($globs as $glob_index => $directory_path) {
141
                        if (!$directory_path) {
142
                            if ($allow_missing_files) {
143
                                continue;
144
                            }
145
146
                            throw new ConfigException(
147
                                'Could not resolve config path to ' . $base_dir
148
                                    . DIRECTORY_SEPARATOR . (string)$directory['name'] . ':' . $glob_index
149
                            );
150
                        }
151
152
                        if ($ignore_type_stats && $filter instanceof ProjectFileFilter) {
153
                            $filter->ignore_type_stats[$directory_path] = true;
0 ignored issues
show
Bug introduced by
The property ignore_type_stats cannot be accessed from this context as it is declared protected in class Psalm\Config\FileFilter.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
154
                        }
155
156
                        if ($declare_strict_types && $filter instanceof ProjectFileFilter) {
157
                            $filter->declare_strict_types[$directory_path] = true;
0 ignored issues
show
Bug introduced by
The property declare_strict_types cannot be accessed from this context as it is declared protected in class Psalm\Config\FileFilter.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
158
                        }
159
160
                        $filter->addDirectory($directory_path);
161
                    }
162
                    continue;
163
                }
164
165
                $directory_path = realpath($prospective_directory_path);
166
167 View Code Duplication
                if (!$directory_path) {
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...
168
                    if ($allow_missing_files) {
169
                        continue;
170
                    }
171
172
                    throw new ConfigException(
173
                        'Could not resolve config path to ' . $base_dir
174
                            . DIRECTORY_SEPARATOR . (string)$directory['name']
175
                    );
176
                }
177
178 View Code Duplication
                if (!is_dir($directory_path)) {
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...
179
                    throw new ConfigException(
180
                        $base_dir . DIRECTORY_SEPARATOR . (string)$directory['name']
181
                            . ' is not a directory'
182
                    );
183
                }
184
185
                /** @var \RecursiveDirectoryIterator */
186
                $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory_path));
187
                $iterator->rewind();
188
189
                while ($iterator->valid()) {
190
                    if (!$iterator->isDot() && $iterator->isLink()) {
191
                        $linked_path = readlink($iterator->getPathname());
192
193
                        if (stripos($linked_path, $directory_path) !== 0) {
194
                            if ($ignore_type_stats && $filter instanceof ProjectFileFilter) {
195
                                $filter->ignore_type_stats[$directory_path] = true;
0 ignored issues
show
Bug introduced by
The property ignore_type_stats cannot be accessed from this context as it is declared protected in class Psalm\Config\FileFilter.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
196
                            }
197
198
                            if ($declare_strict_types && $filter instanceof ProjectFileFilter) {
199
                                $filter->declare_strict_types[$directory_path] = true;
0 ignored issues
show
Bug introduced by
The property declare_strict_types cannot be accessed from this context as it is declared protected in class Psalm\Config\FileFilter.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
200
                            }
201
202
                            if (is_dir($linked_path)) {
203
                                $filter->addDirectory($linked_path);
204
                            }
205
                        }
206
                    }
207
208
                    $iterator->next();
209
                }
210
211
                if ($ignore_type_stats && $filter instanceof ProjectFileFilter) {
212
                    $filter->ignore_type_stats[$directory_path] = true;
0 ignored issues
show
Bug introduced by
The property ignore_type_stats cannot be accessed from this context as it is declared protected in class Psalm\Config\FileFilter.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
213
                }
214
215
                if ($declare_strict_types && $filter instanceof ProjectFileFilter) {
216
                    $filter->declare_strict_types[$directory_path] = true;
0 ignored issues
show
Bug introduced by
The property declare_strict_types cannot be accessed from this context as it is declared protected in class Psalm\Config\FileFilter.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
217
                }
218
219
                $filter->addDirectory($directory_path);
220
            }
221
        }
222
223
        if ($e->file) {
224
            /** @var \SimpleXMLElement $file */
225
            foreach ($e->file as $file) {
226
                $file_path = (string) $file['name'];
227
228
                if ($file_path[0] === '/' && DIRECTORY_SEPARATOR === '/') {
229
                    $prospective_file_path = $file_path;
230
                } else {
231
                    $prospective_file_path = $base_dir . DIRECTORY_SEPARATOR . $file_path;
232
                }
233
234
                if (strpos($prospective_file_path, '*') !== false) {
235
                    $globs = array_map(
236
                        'realpath',
237
                        array_filter(
238
                            glob($prospective_file_path, GLOB_NOSORT),
239
                            'file_exists'
240
                        )
241
                    );
242
243 View Code Duplication
                    if (empty($globs)) {
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...
244
                        throw new ConfigException(
245
                            'Could not resolve config path to ' . $base_dir . DIRECTORY_SEPARATOR .
246
                                (string)$file['name']
247
                        );
248
                    }
249
250
                    foreach ($globs as $glob_index => $file_path) {
251 View Code Duplication
                        if (!$file_path) {
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...
252
                            throw new ConfigException(
253
                                'Could not resolve config path to ' . $base_dir . DIRECTORY_SEPARATOR .
254
                                    (string)$file['name'] . ':' . $glob_index
255
                            );
256
                        }
257
                        $filter->addFile($file_path);
258
                    }
259
                    continue;
260
                }
261
262
                $file_path = realpath($prospective_file_path);
263
264 View Code Duplication
                if (!$file_path) {
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...
265
                    throw new ConfigException(
266
                        'Could not resolve config path to ' . $base_dir . DIRECTORY_SEPARATOR .
267
                        (string)$file['name']
268
                    );
269
                }
270
271
                $filter->addFile($file_path);
272
            }
273
        }
274
275
        if ($e->referencedClass) {
276
            /** @var \SimpleXMLElement $referenced_class */
277
            foreach ($e->referencedClass as $referenced_class) {
278
                $class_name = strtolower((string)$referenced_class['name']);
279
280
                if (strpos($class_name, '*') !== false) {
281
                    $regex = '/' . \str_replace('*', '.*', str_replace('\\', '\\\\', $class_name)) . '/i';
282
                    $filter->fq_classlike_patterns[] = $regex;
283
                } else {
284
                    $filter->fq_classlike_names[] = $class_name;
285
                }
286
            }
287
        }
288
289
        if ($e->referencedMethod) {
290
            /** @var \SimpleXMLElement $referenced_method */
291
            foreach ($e->referencedMethod as $referenced_method) {
292
                $method_id = (string)$referenced_method['name'];
293
294
                if (!preg_match('/^[^:]+::[^:]+$/', $method_id) && !static::isRegularExpression($method_id)) {
0 ignored issues
show
Bug introduced by
Since isRegularExpression() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of isRegularExpression() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
295
                    throw new ConfigException(
296
                        'Invalid referencedMethod ' . $method_id
297
                    );
298
                }
299
300
                $filter->method_ids[] = strtolower($method_id);
301
            }
302
        }
303
304
        if ($e->referencedFunction) {
305
            /** @var \SimpleXMLElement $referenced_function */
306
            foreach ($e->referencedFunction as $referenced_function) {
307
                $filter->method_ids[] = strtolower((string)$referenced_function['name']);
308
            }
309
        }
310
311
        if ($e->referencedProperty) {
312
            /** @var \SimpleXMLElement $referenced_property */
313
            foreach ($e->referencedProperty as $referenced_property) {
314
                $filter->property_ids[] = strtolower((string)$referenced_property['name']);
315
            }
316
        }
317
318
        if ($e->referencedVariable) {
319
            /** @var \SimpleXMLElement $referenced_variable */
320
            foreach ($e->referencedVariable as $referenced_variable) {
321
                $filter->var_names[] = strtolower((string)$referenced_variable['name']);
322
            }
323
        }
324
325
        return $filter;
326
    }
327
328
    private static function isRegularExpression(string $string) : bool
329
    {
330
        set_error_handler(
331
            function () : bool {
332
                return false;
333
            },
334
            E_WARNING
335
        );
336
        $is_regexp = preg_match($string, '') !== false;
337
        restore_error_handler();
338
339
        return $is_regexp;
340
    }
341
342
    /**
343
     * @param  string $str
344
     *
345
     * @return string
346
     */
347
    protected static function slashify($str)
348
    {
349
        return preg_replace('/\/?$/', DIRECTORY_SEPARATOR, $str);
350
    }
351
352
    /**
353
     * @param  string  $file_name
354
     * @param  bool $case_sensitive
355
     *
356
     * @return bool
357
     */
358
    public function allows($file_name, $case_sensitive = false)
359
    {
360
        if ($this->inclusive) {
361
            foreach ($this->directories as $include_dir) {
362
                if ($case_sensitive) {
363
                    if (strpos($file_name, $include_dir) === 0) {
364
                        return true;
365
                    }
366
                } else {
367
                    if (stripos($file_name, $include_dir) === 0) {
368
                        return true;
369
                    }
370
                }
371
            }
372
373 View Code Duplication
            if ($case_sensitive) {
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...
374
                if (in_array($file_name, $this->files, true)) {
375
                    return true;
376
                }
377
            } else {
378
                if (in_array(strtolower($file_name), $this->files_lowercase, true)) {
379
                    return true;
380
                }
381
            }
382
383
            return false;
384
        }
385
386
        // exclusive
387
        foreach ($this->directories as $exclude_dir) {
388
            if ($case_sensitive) {
389
                if (strpos($file_name, $exclude_dir) === 0) {
390
                    return false;
391
                }
392
            } else {
393
                if (stripos($file_name, $exclude_dir) === 0) {
394
                    return false;
395
                }
396
            }
397
        }
398
399 View Code Duplication
        if ($case_sensitive) {
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...
400
            if (in_array($file_name, $this->files, true)) {
401
                return false;
402
            }
403
        } else {
404
            if (in_array(strtolower($file_name), $this->files_lowercase, true)) {
405
                return false;
406
            }
407
        }
408
409
        return true;
410
    }
411
412
    /**
413
     * @param  string  $fq_classlike_name
414
     *
415
     * @return bool
416
     */
417
    public function allowsClass($fq_classlike_name)
418
    {
419
        if ($this->fq_classlike_patterns) {
420
            foreach ($this->fq_classlike_patterns as $pattern) {
421
                if (preg_match($pattern, $fq_classlike_name)) {
422
                    return true;
423
                }
424
            }
425
        }
426
427
        return in_array(strtolower($fq_classlike_name), $this->fq_classlike_names, true);
428
    }
429
430
    /**
431
     * @param  string  $method_id
432
     *
433
     * @return bool
434
     */
435
    public function allowsMethod($method_id)
436
    {
437
        if (!$this->method_ids) {
438
            return false;
439
        }
440
441
        if (preg_match('/^[^:]+::[^:]+$/', $method_id)) {
442
            $method_stub = '*::' . explode('::', $method_id)[1];
443
444
            foreach ($this->method_ids as $config_method_id) {
445
                if ($config_method_id === $method_id) {
446
                    return true;
447
                }
448
449
                if ($config_method_id === $method_stub) {
450
                    return true;
451
                }
452
453
                if ($config_method_id[0] === '/' && preg_match($config_method_id, $method_id)) {
454
                    return true;
455
                }
456
            }
457
458
            return false;
459
        }
460
461
        return in_array($method_id, $this->method_ids, true);
462
    }
463
464
    /**
465
     * @param  string  $property_id
466
     *
467
     * @return bool
468
     */
469
    public function allowsProperty($property_id)
470
    {
471
        return in_array(strtolower($property_id), $this->property_ids, true);
472
    }
473
474
    /**
475
     * @param  string  $var_name
476
     *
477
     * @return bool
478
     */
479
    public function allowsVariable($var_name)
480
    {
481
        return in_array(strtolower($var_name), $this->var_names, true);
482
    }
483
484
    /**
485
     * @return array<string>
486
     */
487
    public function getDirectories()
488
    {
489
        return $this->directories;
490
    }
491
492
    /**
493
     * @return array<string>
494
     */
495
    public function getFiles()
496
    {
497
        return $this->files;
498
    }
499
500
    /**
501
     * @param   string $file_name
502
     *
503
     * @return  void
504
     */
505
    public function addFile($file_name)
506
    {
507
        $this->files[] = $file_name;
508
        $this->files_lowercase[] = strtolower($file_name);
509
    }
510
511
    /**
512
     * @param string $dir_name
513
     *
514
     * @return void
515
     */
516
    public function addDirectory($dir_name)
517
    {
518
        $this->directories[] = self::slashify($dir_name);
519
    }
520
}
521