|
1
|
|
|
<?php |
|
2
|
|
|
namespace Psalm; |
|
3
|
|
|
|
|
4
|
|
|
use Psalm\Checker\ClassLikeChecker; |
|
5
|
|
|
use Psalm\Checker\ProjectChecker; |
|
6
|
|
|
use Psalm\Config\IssueHandler; |
|
7
|
|
|
use Psalm\Config\ProjectFileFilter; |
|
8
|
|
|
use Psalm\Exception\ConfigException; |
|
9
|
|
|
use Psalm\Scanner\FileScanner; |
|
10
|
|
|
use SimpleXMLElement; |
|
11
|
|
|
|
|
12
|
|
|
class Config |
|
13
|
|
|
{ |
|
14
|
|
|
const DEFAULT_FILE_NAME = 'psalm.xml'; |
|
15
|
|
|
const REPORT_INFO = 'info'; |
|
16
|
|
|
const REPORT_ERROR = 'error'; |
|
17
|
|
|
const REPORT_SUPPRESS = 'suppress'; |
|
18
|
|
|
|
|
19
|
|
|
/** |
|
20
|
|
|
* @var array<string> |
|
21
|
|
|
*/ |
|
22
|
|
|
public static $ERROR_LEVELS = [ |
|
23
|
|
|
self::REPORT_INFO, |
|
24
|
|
|
self::REPORT_ERROR, |
|
25
|
|
|
self::REPORT_SUPPRESS, |
|
26
|
|
|
]; |
|
27
|
|
|
|
|
28
|
|
|
/** |
|
29
|
|
|
* @var array |
|
30
|
|
|
*/ |
|
31
|
|
|
protected static $MIXED_ISSUES = [ |
|
32
|
|
|
'MixedArgument', |
|
33
|
|
|
'MixedArrayAccess', |
|
34
|
|
|
'MixedArrayAssignment', |
|
35
|
|
|
'MixedArrayOffset', |
|
36
|
|
|
'MixedAssignment', |
|
37
|
|
|
'MixedInferredReturnType', |
|
38
|
|
|
'MixedMethodCall', |
|
39
|
|
|
'MixedOperand', |
|
40
|
|
|
'MixedPropertyFetch', |
|
41
|
|
|
'MixedPropertyAssignment', |
|
42
|
|
|
'MixedReturnStatement', |
|
43
|
|
|
'MixedStringOffsetAssignment', |
|
44
|
|
|
'MixedTypeCoercion', |
|
45
|
|
|
]; |
|
46
|
|
|
|
|
47
|
|
|
/** |
|
48
|
|
|
* @var self|null |
|
49
|
|
|
*/ |
|
50
|
|
|
private static $instance; |
|
51
|
|
|
|
|
52
|
|
|
/** |
|
53
|
|
|
* Whether or not to use types as defined in docblocks |
|
54
|
|
|
* |
|
55
|
|
|
* @var bool |
|
56
|
|
|
*/ |
|
57
|
|
|
public $use_docblock_types = true; |
|
58
|
|
|
|
|
59
|
|
|
/** |
|
60
|
|
|
* Whether or not to use types as defined in property docblocks. |
|
61
|
|
|
* This is distinct from the above because you may want to use |
|
62
|
|
|
* property docblocks, but not function docblocks. |
|
63
|
|
|
* |
|
64
|
|
|
* @var bool |
|
65
|
|
|
*/ |
|
66
|
|
|
public $use_docblock_property_types = true; |
|
67
|
|
|
|
|
68
|
|
|
/** |
|
69
|
|
|
* Whether or not to throw an exception on first error |
|
70
|
|
|
* |
|
71
|
|
|
* @var bool |
|
72
|
|
|
*/ |
|
73
|
|
|
public $throw_exception = false; |
|
74
|
|
|
|
|
75
|
|
|
/** |
|
76
|
|
|
* The directory to store PHP Parser (and other) caches |
|
77
|
|
|
* |
|
78
|
|
|
* @var string |
|
79
|
|
|
*/ |
|
80
|
|
|
public $cache_directory; |
|
81
|
|
|
|
|
82
|
|
|
/** |
|
83
|
|
|
* Whether or not to care about casing of file names |
|
84
|
|
|
* |
|
85
|
|
|
* @var bool |
|
86
|
|
|
*/ |
|
87
|
|
|
public $use_case_sensitive_file_names = false; |
|
88
|
|
|
|
|
89
|
|
|
/** |
|
90
|
|
|
* Path to the autoader |
|
91
|
|
|
* |
|
92
|
|
|
* @var string|null |
|
93
|
|
|
*/ |
|
94
|
|
|
public $autoloader; |
|
95
|
|
|
|
|
96
|
|
|
/** |
|
97
|
|
|
* @var ProjectFileFilter|null |
|
98
|
|
|
*/ |
|
99
|
|
|
protected $project_files; |
|
100
|
|
|
|
|
101
|
|
|
/** |
|
102
|
|
|
* The base directory of this config file |
|
103
|
|
|
* |
|
104
|
|
|
* @var string |
|
105
|
|
|
*/ |
|
106
|
|
|
protected $base_dir; |
|
107
|
|
|
|
|
108
|
|
|
/** |
|
109
|
|
|
* @var array<int, string> |
|
110
|
|
|
*/ |
|
111
|
|
|
private $file_extensions = ['php']; |
|
112
|
|
|
|
|
113
|
|
|
/** |
|
114
|
|
|
* @var array<string, string> |
|
115
|
|
|
*/ |
|
116
|
|
|
private $filetype_scanners = []; |
|
117
|
|
|
|
|
118
|
|
|
/** |
|
119
|
|
|
* @var array<string, string> |
|
120
|
|
|
*/ |
|
121
|
|
|
private $filetype_checkers = []; |
|
122
|
|
|
|
|
123
|
|
|
/** |
|
124
|
|
|
* @var array<string, IssueHandler> |
|
125
|
|
|
*/ |
|
126
|
|
|
private $issue_handlers = []; |
|
127
|
|
|
|
|
128
|
|
|
/** |
|
129
|
|
|
* @var array<int, string> |
|
130
|
|
|
*/ |
|
131
|
|
|
private $mock_classes = []; |
|
132
|
|
|
|
|
133
|
|
|
/** |
|
134
|
|
|
* @var array<int, string> |
|
135
|
|
|
*/ |
|
136
|
|
|
private $stub_files = []; |
|
137
|
|
|
|
|
138
|
|
|
/** |
|
139
|
|
|
* @var bool |
|
140
|
|
|
*/ |
|
141
|
|
|
public $cache_file_hashes_during_run = true; |
|
142
|
|
|
|
|
143
|
|
|
/** |
|
144
|
|
|
* @var bool |
|
145
|
|
|
*/ |
|
146
|
|
|
public $hide_external_errors = true; |
|
147
|
|
|
|
|
148
|
|
|
/** @var bool */ |
|
149
|
|
|
public $allow_includes = true; |
|
150
|
|
|
|
|
151
|
|
|
/** @var bool */ |
|
152
|
|
|
public $totally_typed = false; |
|
153
|
|
|
|
|
154
|
|
|
/** @var bool */ |
|
155
|
|
|
public $strict_binary_operands = false; |
|
156
|
|
|
|
|
157
|
|
|
/** @var bool */ |
|
158
|
|
|
public $add_void_docblocks = true; |
|
159
|
|
|
|
|
160
|
|
|
/** |
|
161
|
|
|
* If true, assert() calls can be used to check types of variables |
|
162
|
|
|
* |
|
163
|
|
|
* @var bool |
|
164
|
|
|
*/ |
|
165
|
|
|
public $use_assert_for_type = false; |
|
166
|
|
|
|
|
167
|
|
|
/** |
|
168
|
|
|
* @var bool |
|
169
|
|
|
*/ |
|
170
|
|
|
public $remember_property_assignments_after_call = true; |
|
171
|
|
|
|
|
172
|
|
|
/** @var bool */ |
|
173
|
|
|
public $use_igbinary = false; |
|
174
|
|
|
|
|
175
|
|
|
/** |
|
176
|
|
|
* Psalm plugins |
|
177
|
|
|
* |
|
178
|
|
|
* @var array<Plugin> |
|
179
|
|
|
*/ |
|
180
|
|
|
private $plugins = []; |
|
181
|
|
|
|
|
182
|
|
|
/** @var array<string, mixed> */ |
|
183
|
|
|
private $predefined_constants; |
|
184
|
|
|
|
|
185
|
|
|
/** @var array<string, bool> */ |
|
186
|
|
|
private $predefined_functions = []; |
|
187
|
|
|
|
|
188
|
|
|
protected function __construct() |
|
189
|
|
|
{ |
|
190
|
|
|
self::$instance = $this; |
|
191
|
|
|
} |
|
192
|
|
|
|
|
193
|
|
|
/** |
|
194
|
|
|
* Gets a Config object from an XML file. |
|
195
|
|
|
* |
|
196
|
|
|
* Searches up a folder hierarchy for the most immediate config. |
|
197
|
|
|
* |
|
198
|
|
|
* @param string $path |
|
199
|
|
|
* @param string $base_dir |
|
200
|
|
|
* @param string $output_format |
|
201
|
|
|
* |
|
202
|
|
|
* @throws ConfigException if a config path is not found |
|
203
|
|
|
* |
|
204
|
|
|
* @return Config |
|
205
|
|
|
*/ |
|
206
|
|
|
public static function getConfigForPath($path, $base_dir, $output_format) |
|
207
|
|
|
{ |
|
208
|
|
|
$dir_path = realpath($path); |
|
209
|
|
|
|
|
210
|
|
|
if ($dir_path === false) { |
|
211
|
|
|
throw new ConfigException('Config not found for path ' . $path); |
|
212
|
|
|
} |
|
213
|
|
|
|
|
214
|
|
|
if (!is_dir($dir_path)) { |
|
215
|
|
|
$dir_path = dirname($dir_path); |
|
216
|
|
|
} |
|
217
|
|
|
|
|
218
|
|
|
$config = null; |
|
219
|
|
|
|
|
220
|
|
|
do { |
|
221
|
|
|
$maybe_path = $dir_path . DIRECTORY_SEPARATOR . Config::DEFAULT_FILE_NAME; |
|
222
|
|
|
|
|
223
|
|
|
if (file_exists($maybe_path)) { |
|
224
|
|
|
$config = self::loadFromXMLFile($maybe_path, $base_dir); |
|
225
|
|
|
|
|
226
|
|
|
break; |
|
227
|
|
|
} |
|
228
|
|
|
|
|
229
|
|
|
$dir_path = dirname($dir_path); |
|
230
|
|
|
} while (dirname($dir_path) !== $dir_path); |
|
231
|
|
|
|
|
232
|
|
|
if (!$config) { |
|
233
|
|
|
if ($output_format === ProjectChecker::TYPE_CONSOLE) { |
|
234
|
|
|
exit( |
|
|
|
|
|
|
235
|
|
|
'Could not locate a config XML file in path ' . $path . '. Have you run \'psalm --init\' ?' . |
|
236
|
|
|
PHP_EOL |
|
237
|
|
|
); |
|
238
|
|
|
} |
|
239
|
|
|
|
|
240
|
|
|
throw new ConfigException('Config not found for path ' . $path); |
|
241
|
|
|
} |
|
242
|
|
|
|
|
243
|
|
|
return $config; |
|
244
|
|
|
} |
|
245
|
|
|
|
|
246
|
|
|
/** |
|
247
|
|
|
* Creates a new config object from the file |
|
248
|
|
|
* |
|
249
|
|
|
* @param string $file_path |
|
250
|
|
|
* @param string $base_dir |
|
251
|
|
|
* |
|
252
|
|
|
* @return self |
|
253
|
|
|
*/ |
|
254
|
|
|
public static function loadFromXMLFile($file_path, $base_dir) |
|
255
|
|
|
{ |
|
256
|
|
|
$file_contents = file_get_contents($file_path); |
|
257
|
|
|
|
|
258
|
|
|
if ($file_contents === false) { |
|
259
|
|
|
throw new \InvalidArgumentException('Cannot open ' . $file_path); |
|
260
|
|
|
} |
|
261
|
|
|
|
|
262
|
|
|
return self::loadFromXML($file_path, $base_dir, $file_contents); |
|
263
|
|
|
} |
|
264
|
|
|
|
|
265
|
|
|
/** |
|
266
|
|
|
* Creates a new config object from an XML string |
|
267
|
|
|
* |
|
268
|
|
|
* @param string $file_path |
|
269
|
|
|
* @param string $base_dir |
|
270
|
|
|
* @param string $file_contents |
|
271
|
|
|
* |
|
272
|
|
|
* @return self |
|
273
|
|
|
* @psalm-suppress MixedArgument |
|
274
|
|
|
* @psalm-suppress MixedPropertyFetch |
|
275
|
|
|
* @psalm-suppress MixedMethodCall |
|
276
|
|
|
* @psalm-suppress MixedAssignment |
|
277
|
|
|
* @psalm-suppress MixedOperand |
|
278
|
|
|
* @psalm-suppress MixedPropertyAssignment |
|
279
|
|
|
*/ |
|
280
|
|
|
public static function loadFromXML($file_path, $base_dir, $file_contents) |
|
281
|
|
|
{ |
|
282
|
|
|
$config = new static(); |
|
283
|
|
|
|
|
284
|
|
|
$config->base_dir = $base_dir; |
|
285
|
|
|
|
|
286
|
|
|
$schema_path = dirname(dirname(__DIR__)) . '/config.xsd'; |
|
287
|
|
|
|
|
288
|
|
|
if (!file_exists($schema_path)) { |
|
289
|
|
|
throw new ConfigException('Cannot locate config schema'); |
|
290
|
|
|
} |
|
291
|
|
|
|
|
292
|
|
|
$dom_document = new \DOMDocument(); |
|
293
|
|
|
$dom_document->loadXML($file_contents); |
|
294
|
|
|
|
|
295
|
|
|
// Enable user error handling |
|
296
|
|
|
libxml_use_internal_errors(true); |
|
297
|
|
|
|
|
298
|
|
|
if (!$dom_document->schemaValidate($schema_path)) { |
|
299
|
|
|
$errors = libxml_get_errors(); |
|
300
|
|
|
foreach ($errors as $error) { |
|
301
|
|
|
if ($error->level === LIBXML_ERR_FATAL || $error->level === LIBXML_ERR_ERROR) { |
|
302
|
|
|
throw new ConfigException( |
|
303
|
|
|
'Error parsing file ' . $error->file . ' on line ' . $error->line . ': ' . $error->message |
|
304
|
|
|
); |
|
305
|
|
|
} |
|
306
|
|
|
} |
|
307
|
|
|
libxml_clear_errors(); |
|
308
|
|
|
} |
|
309
|
|
|
|
|
310
|
|
|
$config_xml = new SimpleXMLElement($file_contents); |
|
311
|
|
|
|
|
312
|
|
View Code Duplication |
if (isset($config_xml['useDocblockTypes'])) { |
|
|
|
|
|
|
313
|
|
|
$attribute_text = (string) $config_xml['useDocblockTypes']; |
|
314
|
|
|
$config->use_docblock_types = $attribute_text === 'true' || $attribute_text === '1'; |
|
315
|
|
|
} |
|
316
|
|
|
|
|
317
|
|
View Code Duplication |
if (isset($config_xml['useDocblockPropertyTypes'])) { |
|
|
|
|
|
|
318
|
|
|
$attribute_text = (string) $config_xml['useDocblockPropertyTypes']; |
|
319
|
|
|
$config->use_docblock_property_types = $attribute_text === 'true' || $attribute_text === '1'; |
|
320
|
|
|
} |
|
321
|
|
|
|
|
322
|
|
View Code Duplication |
if (isset($config_xml['throwExceptionOnError'])) { |
|
|
|
|
|
|
323
|
|
|
$attribute_text = (string) $config_xml['throwExceptionOnError']; |
|
324
|
|
|
$config->throw_exception = $attribute_text === 'true' || $attribute_text === '1'; |
|
325
|
|
|
} |
|
326
|
|
|
|
|
327
|
|
View Code Duplication |
if (isset($config_xml['hideExternalErrors'])) { |
|
|
|
|
|
|
328
|
|
|
$attribute_text = (string) $config_xml['hideExternalErrors']; |
|
329
|
|
|
$config->hide_external_errors = $attribute_text === 'true' || $attribute_text === '1'; |
|
330
|
|
|
} |
|
331
|
|
|
|
|
332
|
|
|
if (isset($config_xml['autoloader'])) { |
|
333
|
|
|
$config->autoloader = (string) $config_xml['autoloader']; |
|
334
|
|
|
} |
|
335
|
|
|
|
|
336
|
|
|
if (isset($config_xml['cacheDirectory'])) { |
|
337
|
|
|
$config->cache_directory = (string)$config_xml['cacheDirectory']; |
|
338
|
|
|
} else { |
|
339
|
|
|
$config->cache_directory = sys_get_temp_dir() . '/psalm'; |
|
340
|
|
|
} |
|
341
|
|
|
|
|
342
|
|
|
if (@mkdir($config->cache_directory, 0777, true) === false && is_dir($config->cache_directory) === false) { |
|
343
|
|
|
trigger_error('Could not create cache directory: ' . $config->cache_directory, E_USER_ERROR); |
|
344
|
|
|
} |
|
345
|
|
|
|
|
346
|
|
View Code Duplication |
if (isset($config_xml['allowFileIncludes'])) { |
|
|
|
|
|
|
347
|
|
|
$attribute_text = (string) $config_xml['allowFileIncludes']; |
|
348
|
|
|
$config->allow_includes = $attribute_text === 'true' || $attribute_text === '1'; |
|
349
|
|
|
} |
|
350
|
|
|
|
|
351
|
|
View Code Duplication |
if (isset($config_xml['totallyTyped'])) { |
|
|
|
|
|
|
352
|
|
|
$attribute_text = (string) $config_xml['totallyTyped']; |
|
353
|
|
|
$config->totally_typed = $attribute_text === 'true' || $attribute_text === '1'; |
|
354
|
|
|
} |
|
355
|
|
|
|
|
356
|
|
View Code Duplication |
if (isset($config_xml['strictBinaryOperands'])) { |
|
|
|
|
|
|
357
|
|
|
$attribute_text = (string) $config_xml['strictBinaryOperands']; |
|
358
|
|
|
$config->strict_binary_operands = $attribute_text === 'true' || $attribute_text === '1'; |
|
359
|
|
|
} |
|
360
|
|
|
|
|
361
|
|
View Code Duplication |
if (isset($config_xml['requireVoidReturnType'])) { |
|
|
|
|
|
|
362
|
|
|
$attribute_text = (string) $config_xml['requireVoidReturnType']; |
|
363
|
|
|
$config->add_void_docblocks = $attribute_text === 'true' || $attribute_text === '1'; |
|
364
|
|
|
} |
|
365
|
|
|
|
|
366
|
|
View Code Duplication |
if (isset($config_xml['useAssertForType'])) { |
|
|
|
|
|
|
367
|
|
|
$attribute_text = (string) $config_xml['useAssertForType']; |
|
368
|
|
|
$config->use_assert_for_type = $attribute_text === 'true' || $attribute_text === '1'; |
|
369
|
|
|
} |
|
370
|
|
|
|
|
371
|
|
View Code Duplication |
if (isset($config_xml['cacheFileContentHashes'])) { |
|
|
|
|
|
|
372
|
|
|
$attribute_text = (string) $config_xml['cacheFileContentHashes']; |
|
373
|
|
|
$config->cache_file_hashes_during_run = $attribute_text === 'true' || $attribute_text === '1'; |
|
374
|
|
|
} |
|
375
|
|
|
|
|
376
|
|
View Code Duplication |
if (isset($config_xml['rememberPropertyAssignmentsAfterCall'])) { |
|
|
|
|
|
|
377
|
|
|
$attribute_text = (string) $config_xml['rememberPropertyAssignmentsAfterCall']; |
|
378
|
|
|
$config->remember_property_assignments_after_call = $attribute_text === 'true' || $attribute_text === '1'; |
|
379
|
|
|
} |
|
380
|
|
|
|
|
381
|
|
|
if (isset($config_xml['serializer'])) { |
|
382
|
|
|
$attribute_text = (string) $config_xml['serializer']; |
|
383
|
|
|
$config->use_igbinary = $attribute_text === 'igbinary'; |
|
384
|
|
|
} |
|
385
|
|
|
|
|
386
|
|
|
if (isset($config_xml->projectFiles)) { |
|
387
|
|
|
$config->project_files = ProjectFileFilter::loadFromXMLElement($config_xml->projectFiles, $base_dir, true); |
|
388
|
|
|
} |
|
389
|
|
|
|
|
390
|
|
|
if (isset($config_xml->fileExtensions)) { |
|
391
|
|
|
$config->file_extensions = []; |
|
392
|
|
|
|
|
393
|
|
|
$config->loadFileExtensions($config_xml->fileExtensions->extension); |
|
394
|
|
|
} |
|
395
|
|
|
|
|
396
|
|
|
if (isset($config_xml->mockClasses) && isset($config_xml->mockClasses->class)) { |
|
397
|
|
|
/** @var \SimpleXMLElement $mock_class */ |
|
398
|
|
|
foreach ($config_xml->mockClasses->class as $mock_class) { |
|
399
|
|
|
$config->mock_classes[] = (string)$mock_class['name']; |
|
400
|
|
|
} |
|
401
|
|
|
} |
|
402
|
|
|
|
|
403
|
|
|
if (isset($config_xml->stubs) && isset($config_xml->stubs->file)) { |
|
404
|
|
|
/** @var \SimpleXMLElement $stub_file */ |
|
405
|
|
|
foreach ($config_xml->stubs->file as $stub_file) { |
|
406
|
|
|
$file_path = realpath($stub_file['name']); |
|
407
|
|
|
|
|
408
|
|
|
if (!$file_path) { |
|
409
|
|
|
throw new Exception\ConfigException( |
|
410
|
|
|
'Cannot resolve stubfile path ' . getcwd() . '/' . $stub_file['name'] |
|
411
|
|
|
); |
|
412
|
|
|
} |
|
413
|
|
|
|
|
414
|
|
|
$config->stub_files[] = $file_path; |
|
415
|
|
|
} |
|
416
|
|
|
} |
|
417
|
|
|
|
|
418
|
|
|
// this plugin loading system borrows heavily from etsy/phan |
|
419
|
|
|
if (isset($config_xml->plugins) && isset($config_xml->plugins->plugin)) { |
|
420
|
|
|
/** @var \SimpleXMLElement $plugin */ |
|
421
|
|
|
foreach ($config_xml->plugins->plugin as $plugin) { |
|
422
|
|
|
$plugin_file_name = $plugin['filename']; |
|
423
|
|
|
|
|
424
|
|
|
$path = $config->base_dir . $plugin_file_name; |
|
425
|
|
|
|
|
426
|
|
|
$config->addPluginPath($path); |
|
427
|
|
|
} |
|
428
|
|
|
} |
|
429
|
|
|
|
|
430
|
|
|
if (isset($config_xml->issueHandlers)) { |
|
431
|
|
|
/** @var \SimpleXMLElement $issue_handler */ |
|
432
|
|
|
foreach ($config_xml->issueHandlers->children() as $key => $issue_handler) { |
|
433
|
|
|
/** @var string $key */ |
|
434
|
|
|
$config->issue_handlers[$key] = IssueHandler::loadFromXMLElement($issue_handler, $base_dir); |
|
435
|
|
|
} |
|
436
|
|
|
} |
|
437
|
|
|
|
|
438
|
|
|
if ($config->autoloader) { |
|
439
|
|
|
/** @psalm-suppress UnresolvableInclude */ |
|
440
|
|
|
require_once($base_dir . DIRECTORY_SEPARATOR . $config->autoloader); |
|
441
|
|
|
} |
|
442
|
|
|
|
|
443
|
|
|
$config->collectPredefinedConstants(); |
|
444
|
|
|
$config->collectPredefinedFunctions(); |
|
445
|
|
|
|
|
446
|
|
|
return $config; |
|
447
|
|
|
} |
|
448
|
|
|
|
|
449
|
|
|
/** |
|
450
|
|
|
* @return $this |
|
451
|
|
|
*/ |
|
452
|
|
|
public static function getInstance() |
|
453
|
|
|
{ |
|
454
|
|
|
if (self::$instance) { |
|
455
|
|
|
return self::$instance; |
|
456
|
|
|
} |
|
457
|
|
|
|
|
458
|
|
|
throw new \UnexpectedValueException('No config initialized'); |
|
459
|
|
|
} |
|
460
|
|
|
|
|
461
|
|
|
/** |
|
462
|
|
|
* @param string $issue_key |
|
463
|
|
|
* @param string $error_level |
|
464
|
|
|
* |
|
465
|
|
|
* @return void |
|
466
|
|
|
*/ |
|
467
|
|
|
public function setCustomErrorLevel($issue_key, $error_level) |
|
468
|
|
|
{ |
|
469
|
|
|
$this->issue_handlers[$issue_key] = new IssueHandler(); |
|
470
|
|
|
$this->issue_handlers[$issue_key]->setErrorLevel($error_level); |
|
471
|
|
|
} |
|
472
|
|
|
|
|
473
|
|
|
/** |
|
474
|
|
|
* @param array<SimpleXMLElement> $extensions |
|
475
|
|
|
* |
|
476
|
|
|
* @throws ConfigException if a Config file could not be found |
|
477
|
|
|
* |
|
478
|
|
|
* @return void |
|
479
|
|
|
*/ |
|
480
|
|
|
private function loadFileExtensions($extensions) |
|
481
|
|
|
{ |
|
482
|
|
|
foreach ($extensions as $extension) { |
|
483
|
|
|
$extension_name = preg_replace('/^\.?/', '', (string)$extension['name']); |
|
484
|
|
|
$this->file_extensions[] = $extension_name; |
|
485
|
|
|
|
|
486
|
|
View Code Duplication |
if (isset($extension['scanner'])) { |
|
|
|
|
|
|
487
|
|
|
$path = $this->base_dir . (string)$extension['scanner']; |
|
488
|
|
|
|
|
489
|
|
|
if (!file_exists($path)) { |
|
490
|
|
|
throw new Exception\ConfigException('Error parsing config: cannot find file ' . $path); |
|
491
|
|
|
} |
|
492
|
|
|
|
|
493
|
|
|
$this->filetype_scanners[$extension_name] = $path; |
|
494
|
|
|
} |
|
495
|
|
|
|
|
496
|
|
View Code Duplication |
if (isset($extension['checker'])) { |
|
|
|
|
|
|
497
|
|
|
$path = $this->base_dir . (string)$extension['checker']; |
|
498
|
|
|
|
|
499
|
|
|
if (!file_exists($path)) { |
|
500
|
|
|
throw new Exception\ConfigException('Error parsing config: cannot find file ' . $path); |
|
501
|
|
|
} |
|
502
|
|
|
|
|
503
|
|
|
$this->filetype_checkers[$extension_name] = $path; |
|
504
|
|
|
} |
|
505
|
|
|
} |
|
506
|
|
|
} |
|
507
|
|
|
|
|
508
|
|
|
/** |
|
509
|
|
|
* Initialises all the plugins (done once the config is fully loaded) |
|
510
|
|
|
* |
|
511
|
|
|
* @return void |
|
512
|
|
|
* @psalm-suppress MixedArrayAccess |
|
513
|
|
|
* @psalm-suppress MixedAssignment |
|
514
|
|
|
* @psalm-suppress MixedOperand |
|
515
|
|
|
*/ |
|
516
|
|
|
public function initializePlugins(ProjectChecker $project_checker) |
|
517
|
|
|
{ |
|
518
|
|
|
$codebase = $project_checker->codebase; |
|
519
|
|
|
|
|
520
|
|
View Code Duplication |
foreach ($this->filetype_scanners as &$path) { |
|
|
|
|
|
|
521
|
|
|
$file_storage = $codebase->createFileStorageForPath($path); |
|
522
|
|
|
$file_to_scan = new FileScanner($path, $this->shortenFileName($path), false); |
|
523
|
|
|
$file_to_scan->scan( |
|
524
|
|
|
$codebase, |
|
525
|
|
|
$codebase->getStatementsForFile($path), |
|
526
|
|
|
$file_storage |
|
527
|
|
|
); |
|
528
|
|
|
|
|
529
|
|
|
$declared_classes = ClassLikeChecker::getClassesForFile($project_checker, $path); |
|
530
|
|
|
|
|
531
|
|
|
if (count($declared_classes) !== 1) { |
|
532
|
|
|
throw new \InvalidArgumentException( |
|
533
|
|
|
'Filetype handlers must have exactly one class in the file - ' . $path . ' has ' . |
|
534
|
|
|
count($declared_classes) |
|
535
|
|
|
); |
|
536
|
|
|
} |
|
537
|
|
|
|
|
538
|
|
|
/** @psalm-suppress UnresolvableInclude */ |
|
539
|
|
|
require_once($path); |
|
540
|
|
|
|
|
541
|
|
|
if (!\Psalm\Checker\ClassChecker::classExtends( |
|
542
|
|
|
$project_checker, |
|
543
|
|
|
$declared_classes[0], |
|
544
|
|
|
'Psalm\\Scanner\\FileScanner' |
|
545
|
|
|
) |
|
546
|
|
|
) { |
|
547
|
|
|
throw new \InvalidArgumentException( |
|
548
|
|
|
'Filetype handlers must extend \Psalm\Checker\FileChecker - ' . $path . ' does not' |
|
549
|
|
|
); |
|
550
|
|
|
} |
|
551
|
|
|
|
|
552
|
|
|
$path = $declared_classes[0]; |
|
553
|
|
|
} |
|
554
|
|
|
|
|
555
|
|
View Code Duplication |
foreach ($this->filetype_checkers as &$path) { |
|
|
|
|
|
|
556
|
|
|
$file_storage = $codebase->createFileStorageForPath($path); |
|
557
|
|
|
$file_to_scan = new FileScanner($path, $this->shortenFileName($path), false); |
|
558
|
|
|
$file_to_scan->scan( |
|
559
|
|
|
$codebase, |
|
560
|
|
|
$codebase->getStatementsForFile($path), |
|
561
|
|
|
$file_storage |
|
562
|
|
|
); |
|
563
|
|
|
|
|
564
|
|
|
$declared_classes = ClassLikeChecker::getClassesForFile($project_checker, $path); |
|
565
|
|
|
|
|
566
|
|
|
if (count($declared_classes) !== 1) { |
|
567
|
|
|
throw new \InvalidArgumentException( |
|
568
|
|
|
'Filetype handlers must have exactly one class in the file - ' . $path . ' has ' . |
|
569
|
|
|
count($declared_classes) |
|
570
|
|
|
); |
|
571
|
|
|
} |
|
572
|
|
|
|
|
573
|
|
|
/** @psalm-suppress UnresolvableInclude */ |
|
574
|
|
|
require_once($path); |
|
575
|
|
|
|
|
576
|
|
|
if (!\Psalm\Checker\ClassChecker::classExtends( |
|
577
|
|
|
$project_checker, |
|
578
|
|
|
$declared_classes[0], |
|
579
|
|
|
'Psalm\\Checker\\FileChecker' |
|
580
|
|
|
) |
|
581
|
|
|
) { |
|
582
|
|
|
throw new \InvalidArgumentException( |
|
583
|
|
|
'Filetype handlers must extend \Psalm\Checker\FileChecker - ' . $path . ' does not' |
|
584
|
|
|
); |
|
585
|
|
|
} |
|
586
|
|
|
|
|
587
|
|
|
$path = $declared_classes[0]; |
|
588
|
|
|
} |
|
589
|
|
|
} |
|
590
|
|
|
|
|
591
|
|
|
/** |
|
592
|
|
|
* @param string $file_name |
|
593
|
|
|
* |
|
594
|
|
|
* @return string |
|
595
|
|
|
*/ |
|
596
|
|
|
public function shortenFileName($file_name) |
|
597
|
|
|
{ |
|
598
|
|
|
return preg_replace('/^' . preg_quote($this->base_dir, DIRECTORY_SEPARATOR) . '/', '', $file_name); |
|
599
|
|
|
} |
|
600
|
|
|
|
|
601
|
|
|
/** |
|
602
|
|
|
* @param string $issue_type |
|
603
|
|
|
* @param string $file_path |
|
604
|
|
|
* |
|
605
|
|
|
* @return bool |
|
606
|
|
|
*/ |
|
607
|
|
|
public function reportIssueInFile($issue_type, $file_path) |
|
608
|
|
|
{ |
|
609
|
|
|
if (!$this->totally_typed && in_array($issue_type, self::$MIXED_ISSUES, true)) { |
|
610
|
|
|
return false; |
|
611
|
|
|
} |
|
612
|
|
|
|
|
613
|
|
|
if ($this->hide_external_errors) { |
|
614
|
|
|
$codebase = ProjectChecker::getInstance()->codebase; |
|
615
|
|
|
|
|
616
|
|
|
if (!$codebase->canReportIssues($file_path)) { |
|
617
|
|
|
return false; |
|
618
|
|
|
} |
|
619
|
|
|
} |
|
620
|
|
|
|
|
621
|
|
|
if ($this->getReportingLevelForFile($issue_type, $file_path) === self::REPORT_SUPPRESS) { |
|
622
|
|
|
return false; |
|
623
|
|
|
} |
|
624
|
|
|
|
|
625
|
|
|
return true; |
|
626
|
|
|
} |
|
627
|
|
|
|
|
628
|
|
|
/** |
|
629
|
|
|
* @param string $file_path |
|
630
|
|
|
* |
|
631
|
|
|
* @return bool |
|
632
|
|
|
*/ |
|
633
|
|
|
public function isInProjectDirs($file_path) |
|
634
|
|
|
{ |
|
635
|
|
|
return $this->project_files && $this->project_files->allows($file_path); |
|
636
|
|
|
} |
|
637
|
|
|
|
|
638
|
|
|
/** |
|
639
|
|
|
* @param string $issue_type |
|
640
|
|
|
* @param string $file_path |
|
641
|
|
|
* |
|
642
|
|
|
* @return string |
|
643
|
|
|
*/ |
|
644
|
|
|
public function getReportingLevelForFile($issue_type, $file_path) |
|
645
|
|
|
{ |
|
646
|
|
|
if (isset($this->issue_handlers[$issue_type])) { |
|
647
|
|
|
return $this->issue_handlers[$issue_type]->getReportingLevelForFile($file_path); |
|
648
|
|
|
} |
|
649
|
|
|
|
|
650
|
|
|
return self::REPORT_ERROR; |
|
651
|
|
|
} |
|
652
|
|
|
|
|
653
|
|
|
/** |
|
654
|
|
|
* @return array<string> |
|
655
|
|
|
*/ |
|
656
|
|
|
public function getProjectDirectories() |
|
657
|
|
|
{ |
|
658
|
|
|
if (!$this->project_files) { |
|
659
|
|
|
return []; |
|
660
|
|
|
} |
|
661
|
|
|
|
|
662
|
|
|
return $this->project_files->getDirectories(); |
|
663
|
|
|
} |
|
664
|
|
|
|
|
665
|
|
|
/** |
|
666
|
|
|
* @return array<string> |
|
667
|
|
|
*/ |
|
668
|
|
|
public function getFileExtensions() |
|
669
|
|
|
{ |
|
670
|
|
|
return $this->file_extensions; |
|
671
|
|
|
} |
|
672
|
|
|
|
|
673
|
|
|
/** |
|
674
|
|
|
* @return array<string, string> |
|
|
|
|
|
|
675
|
|
|
*/ |
|
676
|
|
|
public function getFiletypeScanners() |
|
677
|
|
|
{ |
|
678
|
|
|
return $this->filetype_scanners; |
|
679
|
|
|
} |
|
680
|
|
|
|
|
681
|
|
|
/** |
|
682
|
|
|
* @return array<string, string> |
|
|
|
|
|
|
683
|
|
|
*/ |
|
684
|
|
|
public function getFiletypeCheckers() |
|
685
|
|
|
{ |
|
686
|
|
|
return $this->filetype_checkers; |
|
687
|
|
|
} |
|
688
|
|
|
|
|
689
|
|
|
/** |
|
690
|
|
|
* @return array<int, string> |
|
|
|
|
|
|
691
|
|
|
*/ |
|
692
|
|
|
public function getMockClasses() |
|
693
|
|
|
{ |
|
694
|
|
|
return $this->mock_classes; |
|
695
|
|
|
} |
|
696
|
|
|
|
|
697
|
|
|
/** |
|
698
|
|
|
* @param ProjectChecker $project_checker |
|
|
|
|
|
|
699
|
|
|
* |
|
700
|
|
|
* @return void |
|
701
|
|
|
*/ |
|
702
|
|
|
public function visitStubFiles(Codebase $codebase) |
|
703
|
|
|
{ |
|
704
|
|
|
$codebase->register_global_functions = true; |
|
705
|
|
|
|
|
706
|
|
|
$generic_stubs_path = realpath(__DIR__ . '/Stubs/CoreGenericFunctions.php'); |
|
707
|
|
|
|
|
708
|
|
|
if (!$generic_stubs_path) { |
|
709
|
|
|
throw new \UnexpectedValueException('Cannot locate core generic stubs'); |
|
710
|
|
|
} |
|
711
|
|
|
|
|
712
|
|
|
$file_storage = $codebase->createFileStorageForPath($generic_stubs_path); |
|
713
|
|
|
$file_to_scan = new FileScanner($generic_stubs_path, $this->shortenFileName($generic_stubs_path), false); |
|
714
|
|
|
$file_to_scan->scan( |
|
715
|
|
|
$codebase, |
|
716
|
|
|
$codebase->getStatementsForFile($generic_stubs_path), |
|
717
|
|
|
$file_storage |
|
718
|
|
|
); |
|
719
|
|
|
|
|
720
|
|
|
foreach ($this->stub_files as $stub_file_path) { |
|
721
|
|
|
$file_storage = $codebase->createFileStorageForPath($stub_file_path); |
|
722
|
|
|
$file_to_scan = new FileScanner($stub_file_path, $this->shortenFileName($stub_file_path), false); |
|
723
|
|
|
$file_to_scan->scan( |
|
724
|
|
|
$codebase, |
|
725
|
|
|
$codebase->getStatementsForFile($stub_file_path), |
|
726
|
|
|
$file_storage |
|
727
|
|
|
); |
|
728
|
|
|
} |
|
729
|
|
|
|
|
730
|
|
|
$codebase->register_global_functions = false; |
|
731
|
|
|
} |
|
732
|
|
|
|
|
733
|
|
|
/** |
|
734
|
|
|
* @return string |
|
735
|
|
|
*/ |
|
736
|
|
|
public function getCacheDirectory() |
|
737
|
|
|
{ |
|
738
|
|
|
return $this->cache_directory; |
|
739
|
|
|
} |
|
740
|
|
|
|
|
741
|
|
|
/** |
|
742
|
|
|
* @return array<Plugin> |
|
743
|
|
|
*/ |
|
744
|
|
|
public function getPlugins() |
|
745
|
|
|
{ |
|
746
|
|
|
return $this->plugins; |
|
747
|
|
|
} |
|
748
|
|
|
|
|
749
|
|
|
/** |
|
750
|
|
|
* @return array<string, mixed> |
|
|
|
|
|
|
751
|
|
|
*/ |
|
752
|
|
|
public function getPredefinedConstants() |
|
753
|
|
|
{ |
|
754
|
|
|
return $this->predefined_constants; |
|
755
|
|
|
} |
|
756
|
|
|
|
|
757
|
|
|
/** |
|
758
|
|
|
* @return void |
|
759
|
|
|
* @psalm-suppress MixedTypeCoercion |
|
760
|
|
|
*/ |
|
761
|
|
|
public function collectPredefinedConstants() |
|
762
|
|
|
{ |
|
763
|
|
|
$this->predefined_constants = get_defined_constants(); |
|
764
|
|
|
} |
|
765
|
|
|
|
|
766
|
|
|
/** |
|
767
|
|
|
* @return array<string, bool> |
|
|
|
|
|
|
768
|
|
|
*/ |
|
769
|
|
|
public function getPredefinedFunctions() |
|
770
|
|
|
{ |
|
771
|
|
|
return $this->predefined_functions; |
|
772
|
|
|
} |
|
773
|
|
|
|
|
774
|
|
|
/** |
|
775
|
|
|
* @return void |
|
776
|
|
|
* @psalm-suppress InvalidPropertyAssignment |
|
777
|
|
|
* @psalm-suppress MixedAssignment |
|
778
|
|
|
* @psalm-suppress MixedArrayOffset |
|
779
|
|
|
*/ |
|
780
|
|
|
public function collectPredefinedFunctions() |
|
781
|
|
|
{ |
|
782
|
|
|
$defined_functions = get_defined_functions(); |
|
783
|
|
|
|
|
784
|
|
View Code Duplication |
if (isset($defined_functions['user'])) { |
|
|
|
|
|
|
785
|
|
|
foreach ($defined_functions['user'] as $function_name) { |
|
786
|
|
|
$this->predefined_functions[$function_name] = true; |
|
787
|
|
|
} |
|
788
|
|
|
} |
|
789
|
|
|
|
|
790
|
|
View Code Duplication |
if (isset($defined_functions['internal'])) { |
|
|
|
|
|
|
791
|
|
|
foreach ($defined_functions['internal'] as $function_name) { |
|
792
|
|
|
$this->predefined_functions[$function_name] = true; |
|
793
|
|
|
} |
|
794
|
|
|
} |
|
795
|
|
|
} |
|
796
|
|
|
|
|
797
|
|
|
/** |
|
798
|
|
|
* @return void |
|
799
|
|
|
* |
|
800
|
|
|
* @psalm-suppress MixedAssignment |
|
801
|
|
|
* @psalm-suppress MixedArrayAccess |
|
802
|
|
|
*/ |
|
803
|
|
|
public function visitComposerAutoloadFiles(ProjectChecker $project_checker) |
|
804
|
|
|
{ |
|
805
|
|
|
$composer_json_path = $this->base_dir . 'composer.json'; // this should ideally not be hardcoded |
|
806
|
|
|
|
|
807
|
|
|
if (!file_exists($composer_json_path)) { |
|
808
|
|
|
return; |
|
809
|
|
|
} |
|
810
|
|
|
|
|
811
|
|
|
/** @psalm-suppress PossiblyFalseArgument */ |
|
812
|
|
|
if (!$composer_json = json_decode(file_get_contents($composer_json_path), true)) { |
|
813
|
|
|
throw new \UnexpectedValueException('Invalid composer.json at ' . $composer_json_path); |
|
814
|
|
|
} |
|
815
|
|
|
|
|
816
|
|
|
if (isset($composer_json['autoload']['files'])) { |
|
817
|
|
|
$codebase = $project_checker->codebase; |
|
818
|
|
|
$codebase->register_global_functions = true; |
|
819
|
|
|
|
|
820
|
|
|
/** @var string[] */ |
|
821
|
|
|
$files = $composer_json['autoload']['files']; |
|
822
|
|
|
|
|
823
|
|
|
foreach ($files as $file) { |
|
824
|
|
|
$file_path = realpath($this->base_dir . $file); |
|
825
|
|
|
|
|
826
|
|
|
if (!$file_path) { |
|
827
|
|
|
continue; |
|
828
|
|
|
} |
|
829
|
|
|
|
|
830
|
|
|
$file_storage = $codebase->createFileStorageForPath($file_path); |
|
831
|
|
|
$file_to_scan = new \Psalm\Scanner\FileScanner($file_path, $this->shortenFileName($file_path), false); |
|
832
|
|
|
$file_to_scan->scan( |
|
833
|
|
|
$codebase, |
|
834
|
|
|
$codebase->getStatementsForFile($file_path), |
|
835
|
|
|
$file_storage |
|
836
|
|
|
); |
|
837
|
|
|
} |
|
838
|
|
|
|
|
839
|
|
|
$project_checker->codebase->register_global_functions = false; |
|
840
|
|
|
} |
|
841
|
|
|
} |
|
842
|
|
|
|
|
843
|
|
|
/** |
|
844
|
|
|
* @param string $current_dir |
|
845
|
|
|
* |
|
846
|
|
|
* @return string |
|
847
|
|
|
* |
|
848
|
|
|
* @psalm-suppress PossiblyFalseArgument |
|
849
|
|
|
* @psalm-suppress MixedArrayAccess |
|
850
|
|
|
* @psalm-suppress MixedAssignment |
|
851
|
|
|
*/ |
|
852
|
|
View Code Duplication |
private static function getVendorDir($current_dir) |
|
|
|
|
|
|
853
|
|
|
{ |
|
854
|
|
|
$composer_json_path = $current_dir . DIRECTORY_SEPARATOR . 'composer.json'; // this should ideally not be hardcoded |
|
855
|
|
|
|
|
856
|
|
|
if (!file_exists($composer_json_path)) { |
|
857
|
|
|
return 'vendor'; |
|
858
|
|
|
} |
|
859
|
|
|
|
|
860
|
|
|
if (!$composer_json = json_decode(file_get_contents($composer_json_path), true)) { |
|
861
|
|
|
throw new \UnexpectedValueException('Invalid composer.json at ' . $composer_json_path); |
|
862
|
|
|
} |
|
863
|
|
|
|
|
864
|
|
|
if (isset($composer_json['config']['vendor-dir'])) { |
|
865
|
|
|
return (string) $composer_json['config']['vendor-dir']; |
|
866
|
|
|
} |
|
867
|
|
|
|
|
868
|
|
|
return 'vendor'; |
|
869
|
|
|
} |
|
870
|
|
|
|
|
871
|
|
|
/** |
|
872
|
|
|
* @return array<string, string> |
|
|
|
|
|
|
873
|
|
|
*/ |
|
874
|
|
|
public function getComposerClassMap() |
|
875
|
|
|
{ |
|
876
|
|
|
$vendor_dir = realpath($this->base_dir . self::getVendorDir($this->base_dir)); |
|
877
|
|
|
|
|
878
|
|
|
if (!$vendor_dir) { |
|
879
|
|
|
return []; |
|
880
|
|
|
} |
|
881
|
|
|
|
|
882
|
|
|
$autoload_files_classmap = |
|
883
|
|
|
$vendor_dir . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'autoload_classmap.php'; |
|
884
|
|
|
|
|
885
|
|
|
if (!file_exists($autoload_files_classmap)) { |
|
886
|
|
|
return []; |
|
887
|
|
|
} |
|
888
|
|
|
|
|
889
|
|
|
/** |
|
890
|
|
|
* @psalm-suppress MixedAssignment |
|
891
|
|
|
* @psalm-suppress UnresolvableInclude |
|
892
|
|
|
*/ |
|
893
|
|
|
$class_map = include_once $autoload_files_classmap; |
|
894
|
|
|
|
|
895
|
|
|
if (is_array($class_map)) { |
|
896
|
|
|
$composer_classmap = array_change_key_case($class_map); |
|
897
|
|
|
|
|
898
|
|
|
$composer_classmap = array_filter( |
|
899
|
|
|
$composer_classmap, |
|
900
|
|
|
/** |
|
901
|
|
|
* @param string $file_path |
|
902
|
|
|
* |
|
903
|
|
|
* @return bool |
|
904
|
|
|
*/ |
|
905
|
|
|
function ($file_path) use ($vendor_dir) { |
|
906
|
|
|
return strpos($file_path, $vendor_dir) === 0; |
|
907
|
|
|
} |
|
908
|
|
|
); |
|
909
|
|
|
} else { |
|
910
|
|
|
$composer_classmap = []; |
|
911
|
|
|
} |
|
912
|
|
|
|
|
913
|
|
|
return $composer_classmap; |
|
914
|
|
|
} |
|
915
|
|
|
|
|
916
|
|
|
/** |
|
917
|
|
|
* @param string $dir |
|
918
|
|
|
* |
|
919
|
|
|
* @return void |
|
920
|
|
|
*/ |
|
921
|
|
|
public static function removeCacheDirectory($dir) |
|
922
|
|
|
{ |
|
923
|
|
|
if (is_dir($dir)) { |
|
924
|
|
|
$objects = scandir($dir); |
|
925
|
|
|
|
|
926
|
|
|
if ($objects === false) { |
|
927
|
|
|
throw new \UnexpectedValueException('Not expecting false here'); |
|
928
|
|
|
} |
|
929
|
|
|
|
|
930
|
|
|
foreach ($objects as $object) { |
|
931
|
|
|
if ($object != '.' && $object != '..') { |
|
932
|
|
|
if (filetype($dir . '/' . $object) == 'dir') { |
|
933
|
|
|
self::removeCacheDirectory($dir . '/' . $object); |
|
934
|
|
|
} else { |
|
935
|
|
|
unlink($dir . '/' . $object); |
|
936
|
|
|
} |
|
937
|
|
|
} |
|
938
|
|
|
} |
|
939
|
|
|
|
|
940
|
|
|
reset($objects); |
|
941
|
|
|
rmdir($dir); |
|
942
|
|
|
} |
|
943
|
|
|
} |
|
944
|
|
|
|
|
945
|
|
|
/** |
|
946
|
|
|
* @param string $path |
|
947
|
|
|
* |
|
948
|
|
|
* @return void |
|
949
|
|
|
*/ |
|
950
|
|
|
public function addPluginPath($path) |
|
951
|
|
|
{ |
|
952
|
|
|
if (!file_exists($path)) { |
|
953
|
|
|
throw new \InvalidArgumentException('Cannot find file ' . $path); |
|
954
|
|
|
} |
|
955
|
|
|
|
|
956
|
|
|
/** |
|
957
|
|
|
* @var Plugin |
|
958
|
|
|
* @psalm-suppress UnresolvableInclude |
|
959
|
|
|
*/ |
|
960
|
|
|
$loaded_plugin = require_once($path); |
|
961
|
|
|
|
|
962
|
|
|
if (!$loaded_plugin) { |
|
963
|
|
|
throw new \InvalidArgumentException( |
|
964
|
|
|
'Plugins must return an instance of that plugin at the end of the file - ' . |
|
965
|
|
|
$plugin_file_name . ' does not' |
|
966
|
|
|
); |
|
967
|
|
|
} |
|
968
|
|
|
|
|
969
|
|
|
if (!($loaded_plugin instanceof Plugin)) { |
|
970
|
|
|
throw new \InvalidArgumentException( |
|
971
|
|
|
'Plugins must extend \Psalm\Plugin - ' . $path . ' does not' |
|
972
|
|
|
); |
|
973
|
|
|
} |
|
974
|
|
|
|
|
975
|
|
|
$this->plugins[] = $loaded_plugin; |
|
976
|
|
|
} |
|
977
|
|
|
} |
|
978
|
|
|
|
An exit expression should only be used in rare cases. For example, if you write a short command line script.
In most cases however, using an
exitexpression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.