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)) { |
|
|
|
|
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; |
|
|
|
|
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
if ($declare_strict_types && $filter instanceof ProjectFileFilter) { |
157
|
|
|
$filter->declare_strict_types[$directory_path] = true; |
|
|
|
|
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) { |
|
|
|
|
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)) { |
|
|
|
|
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; |
|
|
|
|
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
if ($declare_strict_types && $filter instanceof ProjectFileFilter) { |
199
|
|
|
$filter->declare_strict_types[$directory_path] = true; |
|
|
|
|
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; |
|
|
|
|
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
if ($declare_strict_types && $filter instanceof ProjectFileFilter) { |
216
|
|
|
$filter->declare_strict_types[$directory_path] = true; |
|
|
|
|
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)) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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)) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|
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.