2016-06-06 07:07:50 +02:00
|
|
|
<?php
|
2020-09-13 22:40:31 +02:00
|
|
|
|
2016-07-26 00:37:44 +02:00
|
|
|
namespace Psalm;
|
2016-06-06 07:07:50 +02:00
|
|
|
|
2020-09-13 22:40:31 +02:00
|
|
|
use Composer\Autoload\ClassLoader;
|
2020-01-30 08:20:23 +01:00
|
|
|
use Composer\Semver\Semver;
|
2020-09-13 22:40:31 +02:00
|
|
|
use DOMDocument;
|
|
|
|
use LogicException;
|
|
|
|
use Psalm\Config\IssueHandler;
|
|
|
|
use Psalm\Config\ProjectFileFilter;
|
|
|
|
use Psalm\Config\TaintAnalysisFileFilter;
|
|
|
|
use Psalm\Exception\ConfigException;
|
2020-11-21 01:02:44 +01:00
|
|
|
use Psalm\Exception\ConfigNotFoundException;
|
2020-09-13 22:40:31 +02:00
|
|
|
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
|
|
|
use Psalm\Internal\Analyzer\FileAnalyzer;
|
|
|
|
use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
2020-10-03 08:26:37 +02:00
|
|
|
use Psalm\Internal\Composer;
|
2020-09-13 22:40:31 +02:00
|
|
|
use Psalm\Internal\IncludeCollector;
|
|
|
|
use Psalm\Internal\Scanner\FileScanner;
|
|
|
|
use Psalm\Issue\ArgumentIssue;
|
|
|
|
use Psalm\Issue\ClassIssue;
|
|
|
|
use Psalm\Issue\CodeIssue;
|
|
|
|
use Psalm\Issue\FunctionIssue;
|
|
|
|
use Psalm\Issue\MethodIssue;
|
|
|
|
use Psalm\Issue\PropertyIssue;
|
2020-07-16 15:49:59 +02:00
|
|
|
use Psalm\Issue\VariableIssue;
|
2020-09-13 22:40:31 +02:00
|
|
|
use Psalm\Plugin\Hook;
|
|
|
|
use Psalm\Progress\Progress;
|
|
|
|
use Psalm\Progress\VoidProgress;
|
|
|
|
use SimpleXMLElement;
|
2019-07-08 03:20:12 +02:00
|
|
|
use Webmozart\PathUtil\Path;
|
2020-09-13 22:40:31 +02:00
|
|
|
use XdgBaseDir\Xdg;
|
|
|
|
|
2019-07-05 22:24:00 +02:00
|
|
|
use function array_merge;
|
|
|
|
use function array_pop;
|
2020-09-13 22:40:31 +02:00
|
|
|
use function chdir;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function class_exists;
|
|
|
|
use function count;
|
|
|
|
use function dirname;
|
|
|
|
use function explode;
|
|
|
|
use function file_exists;
|
|
|
|
use function file_get_contents;
|
|
|
|
use function filetype;
|
|
|
|
use function get_class;
|
2020-09-13 22:40:31 +02:00
|
|
|
use function getcwd;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function get_defined_constants;
|
|
|
|
use function get_defined_functions;
|
2020-01-22 17:06:06 +01:00
|
|
|
use function glob;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function in_array;
|
|
|
|
use function intval;
|
2020-09-13 22:40:31 +02:00
|
|
|
use function is_a;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function is_dir;
|
2020-01-22 17:06:06 +01:00
|
|
|
use function is_file;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function json_decode;
|
|
|
|
use function libxml_clear_errors;
|
|
|
|
use function libxml_get_errors;
|
|
|
|
use function libxml_use_internal_errors;
|
|
|
|
use function mkdir;
|
|
|
|
use function phpversion;
|
|
|
|
use function preg_match;
|
|
|
|
use function preg_quote;
|
|
|
|
use function preg_replace;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function realpath;
|
|
|
|
use function reset;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function rmdir;
|
|
|
|
use function scandir;
|
|
|
|
use function sha1;
|
2020-09-13 22:40:31 +02:00
|
|
|
use function simplexml_import_dom;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function strpos;
|
|
|
|
use function strrpos;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function strtolower;
|
|
|
|
use function strtr;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function substr;
|
|
|
|
use function substr_count;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function sys_get_temp_dir;
|
|
|
|
use function trigger_error;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function unlink;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function version_compare;
|
2020-09-13 22:40:31 +02:00
|
|
|
|
|
|
|
use const DIRECTORY_SEPARATOR;
|
|
|
|
use const E_USER_ERROR;
|
|
|
|
use const GLOB_NOSORT;
|
|
|
|
use const LIBXML_ERR_ERROR;
|
|
|
|
use const LIBXML_ERR_FATAL;
|
2020-01-16 21:57:52 +01:00
|
|
|
use const LIBXML_NONET;
|
2020-09-13 22:40:31 +02:00
|
|
|
use const PHP_EOL;
|
2020-05-30 23:02:35 +02:00
|
|
|
use const SCANDIR_SORT_NONE;
|
2020-10-07 15:56:21 +02:00
|
|
|
use function array_map;
|
2020-10-25 21:17:57 +01:00
|
|
|
use function rtrim;
|
|
|
|
use function str_replace;
|
|
|
|
use function array_shift;
|
|
|
|
use function array_pad;
|
|
|
|
use function implode;
|
2019-06-01 14:26:22 +02:00
|
|
|
|
2020-02-17 22:33:28 +01:00
|
|
|
/**
|
|
|
|
* @psalm-suppress PropertyNotSetInConstructor
|
2020-08-06 01:49:09 +02:00
|
|
|
* @psalm-consistent-constructor
|
2020-02-17 22:33:28 +01:00
|
|
|
*/
|
2016-06-06 07:07:50 +02:00
|
|
|
class Config
|
|
|
|
{
|
2020-09-20 18:54:46 +02:00
|
|
|
private const DEFAULT_FILE_NAME = 'psalm.xml';
|
|
|
|
public const REPORT_INFO = 'info';
|
|
|
|
public const REPORT_ERROR = 'error';
|
|
|
|
public const REPORT_SUPPRESS = 'suppress';
|
2016-06-25 00:18:11 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
|
|
|
* @var array<string>
|
|
|
|
*/
|
2016-06-27 21:10:13 +02:00
|
|
|
public static $ERROR_LEVELS = [
|
2016-06-25 00:18:11 +02:00
|
|
|
self::REPORT_INFO,
|
|
|
|
self::REPORT_ERROR,
|
2017-05-27 02:05:57 +02:00
|
|
|
self::REPORT_SUPPRESS,
|
2016-06-25 00:18:11 +02:00
|
|
|
];
|
|
|
|
|
2016-12-19 01:17:39 +01:00
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
2020-09-20 18:54:46 +02:00
|
|
|
private const MIXED_ISSUES = [
|
2016-12-19 01:17:39 +01:00
|
|
|
'MixedArgument',
|
|
|
|
'MixedArrayAccess',
|
2017-11-19 19:42:48 +01:00
|
|
|
'MixedArrayAssignment',
|
2016-12-19 01:17:39 +01:00
|
|
|
'MixedArrayOffset',
|
2019-04-29 18:19:51 +02:00
|
|
|
'MixedArrayTypeCoercion',
|
2016-12-19 01:17:39 +01:00
|
|
|
'MixedAssignment',
|
2019-02-10 22:15:52 +01:00
|
|
|
'MixedFunctionCall',
|
2016-12-19 01:17:39 +01:00
|
|
|
'MixedInferredReturnType',
|
|
|
|
'MixedMethodCall',
|
2016-12-24 03:30:32 +01:00
|
|
|
'MixedOperand',
|
2016-12-19 01:17:39 +01:00
|
|
|
'MixedPropertyFetch',
|
|
|
|
'MixedPropertyAssignment',
|
2018-01-05 03:36:16 +01:00
|
|
|
'MixedReturnStatement',
|
2017-05-27 02:05:57 +02:00
|
|
|
'MixedStringOffsetAssignment',
|
2019-04-26 00:02:19 +02:00
|
|
|
'MixedArgumentTypeCoercion',
|
|
|
|
'MixedPropertyTypeCoercion',
|
|
|
|
'MixedReturnTypeCoercion',
|
2016-12-19 01:17:39 +01:00
|
|
|
];
|
|
|
|
|
2020-10-07 15:56:21 +02:00
|
|
|
/**
|
|
|
|
* These are special object classes that allow any and all properties to be get/set on them
|
|
|
|
* @var array<int, class-string>
|
|
|
|
*/
|
|
|
|
protected $universal_object_crates = [
|
|
|
|
\stdClass::class,
|
|
|
|
SimpleXMLElement::class,
|
|
|
|
];
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
2020-03-24 22:59:48 +01:00
|
|
|
* @var static|null
|
2016-11-02 07:29:00 +01:00
|
|
|
*/
|
2018-01-21 19:38:51 +01:00
|
|
|
private static $instance;
|
2016-06-10 00:08:25 +02:00
|
|
|
|
2016-06-27 04:40:57 +02:00
|
|
|
/**
|
|
|
|
* Whether or not to use types as defined in docblocks
|
2016-11-01 05:39:41 +01:00
|
|
|
*
|
2017-05-27 02:16:18 +02:00
|
|
|
* @var bool
|
2016-06-27 04:40:57 +02:00
|
|
|
*/
|
2016-08-24 06:22:38 +02:00
|
|
|
public $use_docblock_types = true;
|
2016-06-10 00:08:25 +02:00
|
|
|
|
2018-01-13 06:32:20 +01:00
|
|
|
/**
|
|
|
|
* Whether or not to use types as defined in property docblocks.
|
|
|
|
* This is distinct from the above because you may want to use
|
|
|
|
* property docblocks, but not function docblocks.
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
2020-09-18 03:40:19 +02:00
|
|
|
public $use_docblock_property_types = false;
|
2018-01-13 06:32:20 +01:00
|
|
|
|
2016-06-27 04:40:57 +02:00
|
|
|
/**
|
|
|
|
* Whether or not to throw an exception on first error
|
2016-11-01 05:39:41 +01:00
|
|
|
*
|
2017-05-27 02:16:18 +02:00
|
|
|
* @var bool
|
2016-06-27 04:40:57 +02:00
|
|
|
*/
|
|
|
|
public $throw_exception = false;
|
|
|
|
|
2019-09-14 16:13:39 +02:00
|
|
|
/**
|
|
|
|
* Whether or not to load Xdebug stub
|
|
|
|
*
|
|
|
|
* @var bool|null
|
|
|
|
*/
|
|
|
|
public $load_xdebug_stub = null;
|
|
|
|
|
2016-07-26 21:00:40 +02:00
|
|
|
/**
|
2016-11-04 22:45:12 +01:00
|
|
|
* The directory to store PHP Parser (and other) caches
|
|
|
|
*
|
2020-09-04 22:26:33 +02:00
|
|
|
* @var string|null
|
2016-11-04 22:45:12 +01:00
|
|
|
*/
|
2017-01-15 23:20:54 +01:00
|
|
|
public $cache_directory;
|
2016-11-04 22:45:12 +01:00
|
|
|
|
2018-10-15 17:29:57 +02:00
|
|
|
/**
|
|
|
|
* The directory to store all Psalm project caches
|
|
|
|
*
|
2019-04-14 19:04:25 +02:00
|
|
|
* @var string|null
|
2018-10-15 17:29:57 +02:00
|
|
|
*/
|
|
|
|
public $global_cache_directory;
|
|
|
|
|
2016-11-04 22:45:12 +01:00
|
|
|
/**
|
|
|
|
* Path to the autoader
|
2016-11-01 05:39:41 +01:00
|
|
|
*
|
2016-07-26 21:00:40 +02:00
|
|
|
* @var string|null
|
|
|
|
*/
|
|
|
|
public $autoloader;
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
2016-12-30 02:07:42 +01:00
|
|
|
* @var ProjectFileFilter|null
|
2016-11-02 07:29:00 +01:00
|
|
|
*/
|
2017-07-25 23:04:58 +02:00
|
|
|
protected $project_files;
|
2016-06-10 00:08:25 +02:00
|
|
|
|
2020-06-19 06:13:09 +02:00
|
|
|
/**
|
|
|
|
* @var ProjectFileFilter|null
|
|
|
|
*/
|
|
|
|
protected $extra_files;
|
|
|
|
|
2016-09-01 05:12:35 +02:00
|
|
|
/**
|
|
|
|
* The base directory of this config file
|
2016-11-01 05:39:41 +01:00
|
|
|
*
|
2016-09-01 05:12:35 +02:00
|
|
|
* @var string
|
|
|
|
*/
|
2018-10-07 02:11:19 +02:00
|
|
|
public $base_dir;
|
2016-06-06 07:07:50 +02:00
|
|
|
|
2020-01-30 08:20:23 +01:00
|
|
|
/**
|
|
|
|
* The PHP version to assume as declared in the config file
|
|
|
|
*
|
|
|
|
* @var string|null
|
|
|
|
*/
|
|
|
|
private $configured_php_version;
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
|
|
|
* @var array<int, string>
|
|
|
|
*/
|
2017-02-12 00:56:38 +01:00
|
|
|
private $file_extensions = ['php'];
|
2016-06-10 20:47:44 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
2019-01-02 15:00:45 +01:00
|
|
|
* @var array<string, class-string<FileScanner>>
|
2016-11-02 07:29:00 +01:00
|
|
|
*/
|
2018-01-21 19:38:51 +01:00
|
|
|
private $filetype_scanners = [];
|
|
|
|
|
|
|
|
/**
|
2019-01-02 15:00:45 +01:00
|
|
|
* @var array<string, class-string<FileAnalyzer>>
|
2018-01-21 19:38:51 +01:00
|
|
|
*/
|
2018-11-11 18:01:14 +01:00
|
|
|
private $filetype_analyzers = [];
|
2016-06-20 06:38:13 +02:00
|
|
|
|
2018-02-12 02:56:34 +01:00
|
|
|
/**
|
|
|
|
* @var array<string, string>
|
|
|
|
*/
|
|
|
|
private $filetype_scanner_paths = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array<string, string>
|
|
|
|
*/
|
2018-11-11 18:01:14 +01:00
|
|
|
private $filetype_analyzer_paths = [];
|
2018-02-12 02:56:34 +01:00
|
|
|
|
2016-09-09 22:21:49 +02:00
|
|
|
/**
|
2016-12-30 02:07:42 +01:00
|
|
|
* @var array<string, IssueHandler>
|
2016-09-09 22:21:49 +02:00
|
|
|
*/
|
2017-02-12 00:56:38 +01:00
|
|
|
private $issue_handlers = [];
|
2016-06-10 20:47:44 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
|
|
|
* @var array<int, string>
|
|
|
|
*/
|
2017-02-12 00:56:38 +01:00
|
|
|
private $mock_classes = [];
|
2016-06-06 07:07:50 +02:00
|
|
|
|
2020-10-30 00:41:10 +01:00
|
|
|
/**
|
|
|
|
* @var array<string, string>
|
|
|
|
*/
|
|
|
|
private $preloaded_stub_files = [];
|
|
|
|
|
2017-02-01 01:21:33 +01:00
|
|
|
/**
|
2020-06-21 17:43:08 +02:00
|
|
|
* @var array<string, string>
|
2017-02-01 01:21:33 +01:00
|
|
|
*/
|
2017-02-12 00:56:38 +01:00
|
|
|
private $stub_files = [];
|
2017-02-01 01:21:33 +01:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
2017-05-27 02:16:18 +02:00
|
|
|
* @var bool
|
2016-11-02 07:29:00 +01:00
|
|
|
*/
|
2019-09-01 16:56:55 +02:00
|
|
|
public $hide_external_errors = false;
|
2016-09-01 05:12:35 +02:00
|
|
|
|
2016-12-07 00:27:22 +01:00
|
|
|
/** @var bool */
|
2016-12-06 22:41:42 +01:00
|
|
|
public $allow_includes = true;
|
|
|
|
|
2020-02-17 22:33:28 +01:00
|
|
|
/** @var 1|2|3|4|5|6|7|8 */
|
|
|
|
public $level = 1;
|
|
|
|
|
2020-02-18 23:23:48 +01:00
|
|
|
/**
|
2020-02-19 02:08:09 +01:00
|
|
|
* @var ?bool
|
2020-02-18 23:23:48 +01:00
|
|
|
*/
|
2020-02-19 02:30:37 +01:00
|
|
|
public $show_mixed_issues = null;
|
2020-02-18 23:23:48 +01:00
|
|
|
|
2016-12-24 03:30:32 +01:00
|
|
|
/** @var bool */
|
|
|
|
public $strict_binary_operands = false;
|
|
|
|
|
2017-04-15 03:32:14 +02:00
|
|
|
/**
|
2017-05-27 02:16:18 +02:00
|
|
|
* @var bool
|
2017-04-15 03:32:14 +02:00
|
|
|
*/
|
|
|
|
public $remember_property_assignments_after_call = true;
|
|
|
|
|
2017-10-15 18:38:47 +02:00
|
|
|
/** @var bool */
|
|
|
|
public $use_igbinary = false;
|
|
|
|
|
2018-02-01 07:10:27 +01:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $allow_phpstorm_generics = false;
|
|
|
|
|
2018-03-06 17:20:54 +01:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
2018-11-12 18:03:55 +01:00
|
|
|
public $allow_string_standin_for_class = false;
|
2018-03-06 17:20:54 +01:00
|
|
|
|
2018-04-22 04:44:54 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
2018-11-30 21:13:25 +01:00
|
|
|
public $use_phpdoc_method_without_magic_or_parent = false;
|
2018-04-22 04:44:54 +02:00
|
|
|
|
2020-02-02 20:23:30 +01:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $use_phpdoc_property_without_magic_or_parent = false;
|
|
|
|
|
2020-02-25 13:40:46 +01:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
2020-10-05 15:57:54 +02:00
|
|
|
public $skip_checks_on_unresolvable_includes = false;
|
2020-02-25 13:40:46 +01:00
|
|
|
|
2020-06-16 04:36:42 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $seal_all_methods = false;
|
|
|
|
|
2018-04-28 19:05:43 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $memoize_method_calls = false;
|
|
|
|
|
2018-06-01 15:07:22 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $hoist_constants = false;
|
|
|
|
|
2018-06-07 18:23:10 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $add_param_default_to_docblock_type = false;
|
|
|
|
|
2018-06-22 07:13:49 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $check_for_throws_docblock = false;
|
|
|
|
|
2019-03-24 21:17:14 +01:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $check_for_throws_in_global_scope = false;
|
|
|
|
|
2018-12-19 22:15:19 +01:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $ignore_internal_falsable_issues = true;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $ignore_internal_nullable_issues = true;
|
|
|
|
|
2018-06-22 07:26:10 +02:00
|
|
|
/**
|
|
|
|
* @var array<string, bool>
|
|
|
|
*/
|
|
|
|
public $ignored_exceptions = [];
|
|
|
|
|
2019-04-10 16:49:27 +02:00
|
|
|
/**
|
|
|
|
* @var array<string, bool>
|
|
|
|
*/
|
|
|
|
public $ignored_exceptions_in_global_scope = [];
|
|
|
|
|
2019-02-01 00:40:40 +01:00
|
|
|
/**
|
|
|
|
* @var array<string, bool>
|
|
|
|
*/
|
|
|
|
public $ignored_exceptions_and_descendants = [];
|
|
|
|
|
2019-04-10 16:49:27 +02:00
|
|
|
/**
|
|
|
|
* @var array<string, bool>
|
|
|
|
*/
|
|
|
|
public $ignored_exceptions_and_descendants_in_global_scope = [];
|
|
|
|
|
2019-06-10 20:41:21 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $infer_property_types_from_constructor = true;
|
|
|
|
|
2019-09-18 20:21:06 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $ensure_array_string_offsets_exist = false;
|
|
|
|
|
2019-10-04 17:08:08 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $ensure_array_int_offsets_exist = false;
|
|
|
|
|
2018-09-18 22:42:51 +02:00
|
|
|
/**
|
|
|
|
* @var array<string, bool>
|
|
|
|
*/
|
|
|
|
public $forbidden_functions = [];
|
|
|
|
|
2018-09-18 23:08:32 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $forbid_echo = false;
|
|
|
|
|
2019-03-05 21:45:09 +01:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $find_unused_code = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $find_unused_variables = false;
|
|
|
|
|
2020-08-22 16:01:26 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $find_unused_psalm_suppress = false;
|
|
|
|
|
2020-07-10 19:22:03 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $run_taint_analysis = false;
|
|
|
|
|
2020-07-17 16:09:42 +02:00
|
|
|
/** @var bool */
|
|
|
|
public $use_phpstorm_meta_path = true;
|
|
|
|
|
2019-07-06 18:21:39 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
2020-10-05 16:08:41 +02:00
|
|
|
public $resolve_from_config_file = true;
|
2019-07-06 18:21:39 +02:00
|
|
|
|
2020-11-06 00:20:04 +01:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $restrict_return_types = false;
|
|
|
|
|
2020-11-10 18:49:42 +01:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $limit_method_complexity = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
public $max_graph_size = 200;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
public $max_avg_path_length = 60;
|
|
|
|
|
2016-06-18 20:45:55 +02:00
|
|
|
/**
|
2018-02-12 02:56:34 +01:00
|
|
|
* @var string[]
|
|
|
|
*/
|
2018-09-04 21:28:03 +02:00
|
|
|
public $plugin_paths = [];
|
2018-02-12 02:56:34 +01:00
|
|
|
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
/**
|
2019-02-17 15:15:37 +01:00
|
|
|
* @var array<array{class:string,config:?SimpleXMLElement}>
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
*/
|
|
|
|
private $plugin_classes = [];
|
|
|
|
|
2018-02-12 02:56:34 +01:00
|
|
|
/**
|
|
|
|
* Static methods to be called after method checks have completed
|
|
|
|
*
|
2019-01-02 15:00:45 +01:00
|
|
|
* @var class-string<Hook\AfterMethodCallAnalysisInterface>[]
|
2018-02-12 02:56:34 +01:00
|
|
|
*/
|
|
|
|
public $after_method_checks = [];
|
|
|
|
|
2018-06-01 03:59:55 +02:00
|
|
|
/**
|
2020-02-13 13:04:02 +01:00
|
|
|
* Static methods to be called after project function checks have completed
|
|
|
|
*
|
|
|
|
* Called after function calls to functions defined in the project.
|
|
|
|
*
|
|
|
|
* Allows influencing the return type and adding of modifications.
|
2018-06-01 03:59:55 +02:00
|
|
|
*
|
2019-01-02 15:00:45 +01:00
|
|
|
* @var class-string<Hook\AfterFunctionCallAnalysisInterface>[]
|
2018-06-01 03:59:55 +02:00
|
|
|
*/
|
|
|
|
public $after_function_checks = [];
|
|
|
|
|
2020-02-13 13:04:02 +01:00
|
|
|
/**
|
|
|
|
* Static methods to be called after every function call
|
|
|
|
*
|
|
|
|
* Called after each function call, including php internal functions.
|
|
|
|
*
|
|
|
|
* Cannot change the call or influence its return type
|
|
|
|
*
|
|
|
|
* @var class-string<Hook\AfterEveryFunctionCallAnalysisInterface>[]
|
|
|
|
*/
|
|
|
|
public $after_every_function_checks = [];
|
|
|
|
|
|
|
|
|
2018-02-12 02:56:34 +01:00
|
|
|
/**
|
|
|
|
* Static methods to be called after expression checks have completed
|
2016-11-02 07:29:00 +01:00
|
|
|
*
|
2019-01-02 15:00:45 +01:00
|
|
|
* @var class-string<Hook\AfterExpressionAnalysisInterface>[]
|
2016-06-18 20:45:55 +02:00
|
|
|
*/
|
2018-02-12 02:56:34 +01:00
|
|
|
public $after_expression_checks = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Static methods to be called after statement checks have completed
|
|
|
|
*
|
2019-01-02 15:00:45 +01:00
|
|
|
* @var class-string<Hook\AfterStatementAnalysisInterface>[]
|
2018-02-12 02:56:34 +01:00
|
|
|
*/
|
|
|
|
public $after_statement_checks = [];
|
|
|
|
|
2019-05-21 05:14:41 +02:00
|
|
|
/**
|
|
|
|
* Static methods to be called after method checks have completed
|
|
|
|
*
|
|
|
|
* @var class-string<Hook\StringInterpreterInterface>[]
|
|
|
|
*/
|
|
|
|
public $string_interpreters = [];
|
|
|
|
|
2018-02-12 02:56:34 +01:00
|
|
|
/**
|
|
|
|
* Static methods to be called after classlike exists checks have completed
|
|
|
|
*
|
2019-01-02 15:00:45 +01:00
|
|
|
* @var class-string<Hook\AfterClassLikeExistenceCheckInterface>[]
|
2018-02-12 02:56:34 +01:00
|
|
|
*/
|
|
|
|
public $after_classlike_exists_checks = [];
|
|
|
|
|
2019-01-09 14:28:12 +01:00
|
|
|
/**
|
|
|
|
* Static methods to be called after classlike checks have completed
|
|
|
|
*
|
|
|
|
* @var class-string<Hook\AfterClassLikeAnalysisInterface>[]
|
|
|
|
*/
|
|
|
|
public $after_classlike_checks = [];
|
|
|
|
|
2018-02-12 02:56:34 +01:00
|
|
|
/**
|
|
|
|
* Static methods to be called after classlikes have been scanned
|
|
|
|
*
|
2019-01-02 15:00:45 +01:00
|
|
|
* @var class-string<Hook\AfterClassLikeVisitInterface>[]
|
2018-02-12 02:56:34 +01:00
|
|
|
*/
|
|
|
|
public $after_visit_classlikes = [];
|
2016-06-18 20:45:55 +02:00
|
|
|
|
2019-02-24 17:40:06 +01:00
|
|
|
/**
|
|
|
|
* Static methods to be called after codebase has been populated
|
2019-07-05 22:24:00 +02:00
|
|
|
*
|
2019-02-24 17:40:06 +01:00
|
|
|
* @var class-string<Hook\AfterCodebasePopulatedInterface>[]
|
|
|
|
*/
|
|
|
|
public $after_codebase_populated = [];
|
|
|
|
|
2019-03-23 17:47:46 +01:00
|
|
|
/**
|
|
|
|
* Static methods to be called after codebase has been populated
|
2019-07-05 22:24:00 +02:00
|
|
|
*
|
2019-03-23 17:47:46 +01:00
|
|
|
* @var class-string<Hook\AfterAnalysisInterface>[]
|
|
|
|
*/
|
|
|
|
public $after_analysis = [];
|
|
|
|
|
2019-08-18 16:40:07 +02:00
|
|
|
/**
|
2020-08-10 16:49:53 +02:00
|
|
|
* Static methods to be called after a file has been analyzed
|
|
|
|
* @var class-string<Hook\AfterFileAnalysisInterface>[]
|
|
|
|
*/
|
|
|
|
public $after_file_checks = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Static methods to be called before a file is analyzed
|
|
|
|
* @var class-string<Hook\BeforeFileAnalysisInterface>[]
|
2019-08-18 16:40:07 +02:00
|
|
|
*/
|
2020-08-10 16:49:53 +02:00
|
|
|
public $before_file_checks = [];
|
2019-08-18 16:40:07 +02:00
|
|
|
|
2020-08-10 15:58:43 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
2020-08-14 06:27:33 +02:00
|
|
|
public $allow_internal_named_arg_calls = true;
|
2020-08-10 15:58:43 +02:00
|
|
|
|
2020-08-10 18:26:25 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
2020-08-14 06:27:33 +02:00
|
|
|
public $allow_named_arg_calls = true;
|
2020-08-10 18:26:25 +02:00
|
|
|
|
2020-04-28 21:30:51 +02:00
|
|
|
/**
|
|
|
|
* Static methods to be called after functionlike checks have completed
|
|
|
|
*
|
|
|
|
* @var class-string<Hook\AfterFunctionLikeAnalysisInterface>[]
|
|
|
|
*/
|
|
|
|
public $after_functionlike_checks = [];
|
|
|
|
|
2016-11-21 03:49:06 +01:00
|
|
|
/** @var array<string, mixed> */
|
2020-09-07 16:54:29 +02:00
|
|
|
private $predefined_constants = [];
|
2016-11-21 03:49:06 +01:00
|
|
|
|
2019-04-10 00:09:57 +02:00
|
|
|
/** @var array<callable-string, bool> */
|
2017-02-12 00:56:38 +01:00
|
|
|
private $predefined_functions = [];
|
2017-01-16 02:11:02 +01:00
|
|
|
|
2018-03-03 21:19:05 +01:00
|
|
|
/** @var ClassLoader|null */
|
|
|
|
private $composer_class_loader;
|
|
|
|
|
2018-07-13 05:26:08 +02:00
|
|
|
/**
|
|
|
|
* Custom functions that always exit
|
|
|
|
*
|
|
|
|
* @var array<string, bool>
|
|
|
|
*/
|
|
|
|
public $exit_functions = [];
|
|
|
|
|
2018-09-26 00:37:24 +02:00
|
|
|
/**
|
2018-11-01 22:42:48 +01:00
|
|
|
* @var string
|
2018-09-26 00:37:24 +02:00
|
|
|
*/
|
2018-11-01 22:42:48 +01:00
|
|
|
public $hash = '';
|
2018-09-26 00:37:24 +02:00
|
|
|
|
2018-10-30 15:32:20 +01:00
|
|
|
/** @var string|null */
|
|
|
|
public $error_baseline = null;
|
|
|
|
|
2019-07-12 16:31:12 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $include_php_versions_in_error_baseline = false;
|
|
|
|
|
2019-03-23 17:49:37 +01:00
|
|
|
/** @var string */
|
2019-03-31 20:02:30 +02:00
|
|
|
public $shepherd_host = 'shepherd.dev';
|
2019-03-23 17:49:37 +01:00
|
|
|
|
2019-03-06 00:08:41 +01:00
|
|
|
/**
|
|
|
|
* @var array<string, string>
|
|
|
|
*/
|
|
|
|
public $globals = [];
|
|
|
|
|
2019-04-22 16:01:25 +02:00
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
public $max_string_length = 1000;
|
|
|
|
|
2020-07-11 23:17:22 +02:00
|
|
|
/** @var ?IncludeCollector */
|
|
|
|
private $include_collector;
|
|
|
|
|
2020-05-26 05:28:11 +02:00
|
|
|
/**
|
2020-05-26 13:50:11 +02:00
|
|
|
* @var TaintAnalysisFileFilter|null
|
2020-05-26 05:28:11 +02:00
|
|
|
*/
|
|
|
|
protected $taint_analysis_ignored_files;
|
|
|
|
|
2020-06-23 00:16:47 +02:00
|
|
|
/**
|
|
|
|
* @var bool whether to emit a backtrace of emitted issues to stderr
|
|
|
|
*/
|
|
|
|
public $debug_emitted_issues = false;
|
|
|
|
|
2020-09-01 15:22:05 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
private $report_info = true;
|
|
|
|
|
2016-12-14 18:28:38 +01:00
|
|
|
protected function __construct()
|
2016-06-06 07:07:50 +02:00
|
|
|
{
|
2018-01-21 19:38:51 +01:00
|
|
|
self::$instance = $this;
|
2016-06-06 07:07:50 +02:00
|
|
|
}
|
|
|
|
|
2018-01-21 16:22:04 +01:00
|
|
|
/**
|
|
|
|
* Gets a Config object from an XML file.
|
|
|
|
*
|
|
|
|
* Searches up a folder hierarchy for the most immediate config.
|
|
|
|
*
|
2019-08-18 21:34:32 +02:00
|
|
|
* @throws ConfigException if a config path is not found
|
2019-07-06 18:21:39 +02:00
|
|
|
*
|
2018-01-21 16:22:04 +01:00
|
|
|
*/
|
2020-11-21 01:02:44 +01:00
|
|
|
public static function getConfigForPath(string $path, string $current_dir): Config
|
2018-11-01 18:05:33 +01:00
|
|
|
{
|
|
|
|
$config_path = self::locateConfigFile($path);
|
|
|
|
|
|
|
|
if (!$config_path) {
|
2020-11-21 01:02:44 +01:00
|
|
|
throw new ConfigNotFoundException('Config not found for path ' . $path);
|
2018-11-01 18:05:33 +01:00
|
|
|
}
|
|
|
|
|
2019-07-06 18:21:39 +02:00
|
|
|
return self::loadFromXMLFile($config_path, $current_dir);
|
2018-11-01 18:05:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Searches up a folder hierarchy for the most immediate config.
|
|
|
|
*
|
|
|
|
* @throws ConfigException
|
2019-07-05 22:24:00 +02:00
|
|
|
*
|
2018-11-01 18:05:33 +01:00
|
|
|
*/
|
2020-09-04 22:26:33 +02:00
|
|
|
public static function locateConfigFile(string $path): ?string
|
2018-01-21 16:22:04 +01:00
|
|
|
{
|
|
|
|
$dir_path = realpath($path);
|
|
|
|
|
|
|
|
if ($dir_path === false) {
|
2020-11-21 01:02:44 +01:00
|
|
|
throw new ConfigNotFoundException('Config not found for path ' . $path);
|
2018-01-21 16:22:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_dir($dir_path)) {
|
|
|
|
$dir_path = dirname($dir_path);
|
|
|
|
}
|
|
|
|
|
|
|
|
do {
|
|
|
|
$maybe_path = $dir_path . DIRECTORY_SEPARATOR . Config::DEFAULT_FILE_NAME;
|
|
|
|
|
2018-04-14 15:57:51 +02:00
|
|
|
if (file_exists($maybe_path) || file_exists($maybe_path .= '.dist')) {
|
2018-11-01 18:05:33 +01:00
|
|
|
return $maybe_path;
|
2018-01-21 16:22:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$dir_path = dirname($dir_path);
|
|
|
|
} while (dirname($dir_path) !== $dir_path);
|
2019-07-05 22:24:00 +02:00
|
|
|
|
2018-11-01 18:05:33 +01:00
|
|
|
return null;
|
2018-01-21 16:22:04 +01:00
|
|
|
}
|
|
|
|
|
2016-06-26 19:45:20 +02:00
|
|
|
/**
|
|
|
|
* Creates a new config object from the file
|
2016-12-29 16:24:10 +01:00
|
|
|
*/
|
2020-09-07 01:36:47 +02:00
|
|
|
public static function loadFromXMLFile(string $file_path, string $current_dir): Config
|
2016-12-29 16:24:10 +01:00
|
|
|
{
|
|
|
|
$file_contents = file_get_contents($file_path);
|
|
|
|
|
2019-07-06 18:21:39 +02:00
|
|
|
$base_dir = dirname($file_path) . DIRECTORY_SEPARATOR;
|
|
|
|
|
2016-12-29 16:24:10 +01:00
|
|
|
if ($file_contents === false) {
|
|
|
|
throw new \InvalidArgumentException('Cannot open ' . $file_path);
|
|
|
|
}
|
|
|
|
|
2018-04-19 23:29:07 +02:00
|
|
|
try {
|
2019-07-06 18:21:39 +02:00
|
|
|
$config = self::loadFromXML($base_dir, $file_contents, $current_dir);
|
2019-05-08 17:58:34 +02:00
|
|
|
$config->hash = sha1($file_contents . \PSALM_VERSION);
|
2018-04-19 23:29:07 +02:00
|
|
|
} catch (ConfigException $e) {
|
|
|
|
throw new ConfigException(
|
|
|
|
'Problem parsing ' . $file_path . ":\n" . ' ' . $e->getMessage()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $config;
|
2016-12-29 16:24:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new config object from an XML string
|
2019-07-06 18:21:39 +02:00
|
|
|
* @param string|null $current_dir Current working directory, if different to $base_dir
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2020-09-04 22:26:33 +02:00
|
|
|
* @throws ConfigException
|
2016-06-26 19:45:20 +02:00
|
|
|
*/
|
2020-09-07 01:36:47 +02:00
|
|
|
public static function loadFromXML(string $base_dir, string $file_contents, ?string $current_dir = null): Config
|
2016-06-10 00:08:25 +02:00
|
|
|
{
|
2019-07-06 18:21:39 +02:00
|
|
|
if ($current_dir === null) {
|
|
|
|
$current_dir = $base_dir;
|
|
|
|
}
|
2016-06-21 01:30:38 +02:00
|
|
|
|
2020-01-16 21:57:52 +01:00
|
|
|
self::validateXmlConfig($base_dir, $file_contents);
|
2016-06-10 20:47:44 +02:00
|
|
|
|
2019-07-07 14:55:53 +02:00
|
|
|
return self::fromXmlAndPaths($base_dir, $file_contents, $current_dir);
|
|
|
|
}
|
|
|
|
|
2020-01-16 21:57:52 +01:00
|
|
|
private static function loadDomDocument(string $base_dir, string $file_contents): DOMDocument
|
|
|
|
{
|
|
|
|
$dom_document = new DOMDocument();
|
|
|
|
|
|
|
|
// there's no obvious way to set xml:base for a document when loading it from string
|
|
|
|
// so instead we're changing the current directory instead to be able to process XIncludes
|
|
|
|
$oldpwd = getcwd();
|
|
|
|
chdir($base_dir);
|
|
|
|
|
|
|
|
$dom_document->loadXML($file_contents, LIBXML_NONET);
|
|
|
|
$dom_document->xinclude(LIBXML_NONET);
|
|
|
|
|
|
|
|
chdir($oldpwd);
|
|
|
|
return $dom_document;
|
|
|
|
}
|
2019-07-07 14:55:53 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @throws ConfigException
|
|
|
|
*/
|
2020-01-16 21:57:52 +01:00
|
|
|
private static function validateXmlConfig(string $base_dir, string $file_contents): void
|
2019-07-07 14:55:53 +02:00
|
|
|
{
|
2020-09-20 14:55:28 +02:00
|
|
|
$schema_path = dirname(__DIR__, 2). '/config.xsd';
|
2016-12-30 04:11:10 +01:00
|
|
|
|
|
|
|
if (!file_exists($schema_path)) {
|
|
|
|
throw new ConfigException('Cannot locate config schema');
|
|
|
|
}
|
|
|
|
|
2020-01-16 21:57:52 +01:00
|
|
|
$dom_document = self::loadDomDocument($base_dir, $file_contents);
|
2016-12-30 04:11:10 +01:00
|
|
|
|
2018-04-22 01:05:26 +02:00
|
|
|
$psalm_nodes = $dom_document->getElementsByTagName('psalm');
|
|
|
|
|
|
|
|
/** @var \DomElement|null */
|
|
|
|
$psalm_node = $psalm_nodes->item(0);
|
|
|
|
|
|
|
|
if (!$psalm_node) {
|
|
|
|
throw new ConfigException(
|
|
|
|
'Missing psalm node'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$psalm_node->hasAttribute('xmlns')) {
|
|
|
|
$psalm_node->setAttribute('xmlns', 'https://getpsalm.org/schema/config');
|
|
|
|
|
|
|
|
$old_dom_document = $dom_document;
|
2020-01-16 21:57:52 +01:00
|
|
|
$dom_document = self::loadDomDocument($base_dir, $old_dom_document->saveXML());
|
2018-04-22 01:05:26 +02:00
|
|
|
}
|
|
|
|
|
2016-12-30 04:11:10 +01:00
|
|
|
// Enable user error handling
|
|
|
|
libxml_use_internal_errors(true);
|
|
|
|
|
|
|
|
if (!$dom_document->schemaValidate($schema_path)) {
|
|
|
|
$errors = libxml_get_errors();
|
|
|
|
foreach ($errors as $error) {
|
|
|
|
if ($error->level === LIBXML_ERR_FATAL || $error->level === LIBXML_ERR_ERROR) {
|
2017-04-28 06:31:55 +02:00
|
|
|
throw new ConfigException(
|
2018-04-19 23:29:07 +02:00
|
|
|
'Error on line ' . $error->line . ":\n" . ' ' . $error->message
|
2017-04-28 06:31:55 +02:00
|
|
|
);
|
2016-12-30 04:11:10 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
libxml_clear_errors();
|
|
|
|
}
|
2019-07-07 14:55:53 +02:00
|
|
|
}
|
2016-12-30 04:11:10 +01:00
|
|
|
|
2016-06-27 04:40:57 +02:00
|
|
|
|
2019-07-07 14:55:53 +02:00
|
|
|
/**
|
|
|
|
* @psalm-suppress MixedMethodCall
|
|
|
|
* @psalm-suppress MixedAssignment
|
|
|
|
* @psalm-suppress MixedOperand
|
|
|
|
* @psalm-suppress MixedArgument
|
|
|
|
* @psalm-suppress MixedPropertyFetch
|
|
|
|
*
|
|
|
|
* @throws ConfigException
|
|
|
|
*/
|
|
|
|
private static function fromXmlAndPaths(string $base_dir, string $file_contents, string $current_dir): self
|
|
|
|
{
|
|
|
|
$config = new static();
|
2016-09-01 05:12:35 +02:00
|
|
|
|
2020-01-16 21:57:52 +01:00
|
|
|
$dom_document = self::loadDomDocument($base_dir, $file_contents);
|
|
|
|
|
|
|
|
$config_xml = simplexml_import_dom($dom_document);
|
2019-07-06 18:21:39 +02:00
|
|
|
|
2019-07-07 14:55:53 +02:00
|
|
|
$booleanAttributes = [
|
|
|
|
'useDocblockTypes' => 'use_docblock_types',
|
|
|
|
'useDocblockPropertyTypes' => 'use_docblock_property_types',
|
|
|
|
'throwExceptionOnError' => 'throw_exception',
|
|
|
|
'hideExternalErrors' => 'hide_external_errors',
|
|
|
|
'resolveFromConfigFile' => 'resolve_from_config_file',
|
|
|
|
'allowFileIncludes' => 'allow_includes',
|
|
|
|
'strictBinaryOperands' => 'strict_binary_operands',
|
|
|
|
'rememberPropertyAssignmentsAfterCall' => 'remember_property_assignments_after_call',
|
|
|
|
'allowPhpStormGenerics' => 'allow_phpstorm_generics',
|
|
|
|
'allowStringToStandInForClass' => 'allow_string_standin_for_class',
|
|
|
|
'usePhpDocMethodsWithoutMagicCall' => 'use_phpdoc_method_without_magic_or_parent',
|
2020-02-18 16:39:24 +01:00
|
|
|
'usePhpDocPropertiesWithoutMagicCall' => 'use_phpdoc_property_without_magic_or_parent',
|
2019-07-07 14:55:53 +02:00
|
|
|
'memoizeMethodCallResults' => 'memoize_method_calls',
|
|
|
|
'hoistConstants' => 'hoist_constants',
|
|
|
|
'addParamDefaultToDocblockType' => 'add_param_default_to_docblock_type',
|
|
|
|
'checkForThrowsDocblock' => 'check_for_throws_docblock',
|
|
|
|
'checkForThrowsInGlobalScope' => 'check_for_throws_in_global_scope',
|
|
|
|
'forbidEcho' => 'forbid_echo',
|
|
|
|
'ignoreInternalFunctionFalseReturn' => 'ignore_internal_falsable_issues',
|
|
|
|
'ignoreInternalFunctionNullReturn' => 'ignore_internal_nullable_issues',
|
2019-07-12 16:31:12 +02:00
|
|
|
'includePhpVersionsInErrorBaseline' => 'include_php_versions_in_error_baseline',
|
2019-09-14 16:13:39 +02:00
|
|
|
'loadXdebugStub' => 'load_xdebug_stub',
|
2019-09-18 20:21:06 +02:00
|
|
|
'ensureArrayStringOffsetsExist' => 'ensure_array_string_offsets_exist',
|
2019-10-04 17:08:08 +02:00
|
|
|
'ensureArrayIntOffsetsExist' => 'ensure_array_int_offsets_exist',
|
2020-02-19 02:30:37 +01:00
|
|
|
'reportMixedIssues' => 'show_mixed_issues',
|
2020-06-16 04:36:42 +02:00
|
|
|
'skipChecksOnUnresolvableIncludes' => 'skip_checks_on_unresolvable_includes',
|
2020-07-10 19:22:03 +02:00
|
|
|
'sealAllMethods' => 'seal_all_methods',
|
|
|
|
'runTaintAnalysis' => 'run_taint_analysis',
|
2020-07-17 16:09:42 +02:00
|
|
|
'usePhpStormMetaPath' => 'use_phpstorm_meta_path',
|
2020-08-14 06:27:33 +02:00
|
|
|
'allowInternalNamedArgumentsCalls' => 'allow_internal_named_arg_calls',
|
|
|
|
'allowNamedArgumentCalls' => 'allow_named_arg_calls',
|
2020-08-22 16:01:26 +02:00
|
|
|
'findUnusedPsalmSuppress' => 'find_unused_psalm_suppress',
|
2020-09-01 15:22:05 +02:00
|
|
|
'reportInfo' => 'report_info',
|
2020-11-06 00:20:04 +01:00
|
|
|
'restrictReturnTypes' => 'restrict_return_types',
|
2020-11-10 18:49:42 +01:00
|
|
|
'limitMethodComplexity' => 'limit_method_complexity',
|
2019-07-07 14:55:53 +02:00
|
|
|
];
|
|
|
|
|
|
|
|
foreach ($booleanAttributes as $xmlName => $internalName) {
|
|
|
|
if (isset($config_xml[$xmlName])) {
|
|
|
|
$attribute_text = (string) $config_xml[$xmlName];
|
|
|
|
$config->setBooleanAttribute(
|
|
|
|
$internalName,
|
|
|
|
$attribute_text === 'true' || $attribute_text === '1'
|
|
|
|
);
|
|
|
|
}
|
2019-07-06 18:21:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($config->resolve_from_config_file) {
|
|
|
|
$config->base_dir = $base_dir;
|
|
|
|
} else {
|
|
|
|
$config->base_dir = $current_dir;
|
|
|
|
$base_dir = $current_dir;
|
|
|
|
}
|
|
|
|
|
2020-01-30 08:20:23 +01:00
|
|
|
if (isset($config_xml['phpVersion'])) {
|
|
|
|
$config->configured_php_version = (string) $config_xml['phpVersion'];
|
|
|
|
}
|
|
|
|
|
2016-07-26 21:00:40 +02:00
|
|
|
if (isset($config_xml['autoloader'])) {
|
2019-01-02 17:18:22 +01:00
|
|
|
$autoloader_path = $config->base_dir . DIRECTORY_SEPARATOR . $config_xml['autoloader'];
|
|
|
|
|
|
|
|
if (!file_exists($autoloader_path)) {
|
2019-01-16 08:05:58 +01:00
|
|
|
throw new ConfigException('Cannot locate autoloader');
|
2019-01-02 17:18:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$config->autoloader = realpath($autoloader_path);
|
2016-07-26 21:00:40 +02:00
|
|
|
}
|
|
|
|
|
2016-11-04 22:45:12 +01:00
|
|
|
if (isset($config_xml['cacheDirectory'])) {
|
2017-01-16 21:58:35 +01:00
|
|
|
$config->cache_directory = (string)$config_xml['cacheDirectory'];
|
2020-09-01 15:20:24 +02:00
|
|
|
} elseif ($user_cache_dir = (new Xdg())->getHomeCacheDir()) {
|
|
|
|
$config->cache_directory = $user_cache_dir . '/psalm';
|
2017-01-15 23:20:54 +01:00
|
|
|
} else {
|
|
|
|
$config->cache_directory = sys_get_temp_dir() . '/psalm';
|
2016-11-04 22:45:12 +01:00
|
|
|
}
|
|
|
|
|
2018-10-15 17:29:57 +02:00
|
|
|
$config->global_cache_directory = $config->cache_directory;
|
|
|
|
|
|
|
|
$config->cache_directory .= DIRECTORY_SEPARATOR . sha1($base_dir);
|
|
|
|
|
2019-03-31 17:17:00 +02:00
|
|
|
if (is_dir($config->cache_directory) === false && @mkdir($config->cache_directory, 0777, true) === false) {
|
2017-01-16 21:58:35 +01:00
|
|
|
trigger_error('Could not create cache directory: ' . $config->cache_directory, E_USER_ERROR);
|
|
|
|
}
|
|
|
|
|
2017-10-15 18:38:47 +02:00
|
|
|
if (isset($config_xml['serializer'])) {
|
|
|
|
$attribute_text = (string) $config_xml['serializer'];
|
|
|
|
$config->use_igbinary = $attribute_text === 'igbinary';
|
2018-02-19 17:14:07 +01:00
|
|
|
} elseif ($igbinary_version = phpversion('igbinary')) {
|
|
|
|
$config->use_igbinary = version_compare($igbinary_version, '2.0.5') >= 0;
|
2017-10-15 18:38:47 +02:00
|
|
|
}
|
|
|
|
|
2018-09-18 23:08:32 +02:00
|
|
|
|
2019-03-05 21:45:09 +01:00
|
|
|
if (isset($config_xml['findUnusedCode'])) {
|
|
|
|
$attribute_text = (string) $config_xml['findUnusedCode'];
|
|
|
|
$config->find_unused_code = $attribute_text === 'true' || $attribute_text === '1';
|
|
|
|
$config->find_unused_variables = $config->find_unused_code;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isset($config_xml['findUnusedVariablesAndParams'])) {
|
|
|
|
$attribute_text = (string) $config_xml['findUnusedVariablesAndParams'];
|
|
|
|
$config->find_unused_variables = $attribute_text === 'true' || $attribute_text === '1';
|
|
|
|
}
|
|
|
|
|
2020-02-18 04:43:13 +01:00
|
|
|
if (isset($config_xml['errorLevel'])) {
|
|
|
|
$attribute_text = (int) $config_xml['errorLevel'];
|
2020-02-17 22:33:28 +01:00
|
|
|
|
|
|
|
if (!in_array($attribute_text, [1, 2, 3, 4, 5, 6, 7, 8], true)) {
|
|
|
|
throw new Exception\ConfigException(
|
2020-02-18 04:43:13 +01:00
|
|
|
'Invalid error level ' . $config_xml['errorLevel']
|
2020-02-17 22:33:28 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$config->level = $attribute_text;
|
2020-02-18 00:43:40 +01:00
|
|
|
} elseif (isset($config_xml['totallyTyped'])) {
|
2020-02-18 15:40:21 +01:00
|
|
|
$totally_typed = (string) $config_xml['totallyTyped'];
|
2020-02-18 00:37:01 +01:00
|
|
|
|
|
|
|
if ($totally_typed === 'true' || $totally_typed === '1') {
|
|
|
|
$config->level = 1;
|
|
|
|
} else {
|
|
|
|
$config->level = 2;
|
2020-02-19 02:08:09 +01:00
|
|
|
|
2020-02-19 02:30:37 +01:00
|
|
|
if ($config->show_mixed_issues === null) {
|
|
|
|
$config->show_mixed_issues = false;
|
2020-02-19 02:08:09 +01:00
|
|
|
}
|
2020-02-18 00:37:01 +01:00
|
|
|
}
|
2020-02-18 00:43:40 +01:00
|
|
|
} else {
|
|
|
|
$config->level = 2;
|
2020-02-17 22:33:28 +01:00
|
|
|
}
|
|
|
|
|
2018-10-30 15:32:20 +01:00
|
|
|
if (isset($config_xml['errorBaseline'])) {
|
|
|
|
$attribute_text = (string) $config_xml['errorBaseline'];
|
|
|
|
$config->error_baseline = $attribute_text;
|
|
|
|
}
|
|
|
|
|
2019-04-22 16:01:25 +02:00
|
|
|
if (isset($config_xml['maxStringLength'])) {
|
|
|
|
$attribute_text = intval($config_xml['maxStringLength']);
|
|
|
|
$config->max_string_length = $attribute_text;
|
|
|
|
}
|
|
|
|
|
2019-06-10 20:41:21 +02:00
|
|
|
if (isset($config_xml['inferPropertyTypesFromConstructor'])) {
|
|
|
|
$attribute_text = (string) $config_xml['inferPropertyTypesFromConstructor'];
|
|
|
|
$config->infer_property_types_from_constructor = $attribute_text === 'true' || $attribute_text === '1';
|
|
|
|
}
|
|
|
|
|
2016-12-29 14:42:39 +01:00
|
|
|
if (isset($config_xml->projectFiles)) {
|
2017-05-04 20:25:58 +02:00
|
|
|
$config->project_files = ProjectFileFilter::loadFromXMLElement($config_xml->projectFiles, $base_dir, true);
|
2016-06-10 00:08:25 +02:00
|
|
|
}
|
|
|
|
|
2020-06-19 06:13:09 +02:00
|
|
|
if (isset($config_xml->extraFiles)) {
|
|
|
|
$config->extra_files = ProjectFileFilter::loadFromXMLElement($config_xml->extraFiles, $base_dir, true);
|
|
|
|
}
|
|
|
|
|
2020-05-26 05:28:11 +02:00
|
|
|
if (isset($config_xml->taintAnalysis->ignoreFiles)) {
|
|
|
|
$config->taint_analysis_ignored_files = TaintAnalysisFileFilter::loadFromXMLElement(
|
|
|
|
$config_xml->taintAnalysis->ignoreFiles,
|
|
|
|
$base_dir,
|
|
|
|
false
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-06-10 00:08:25 +02:00
|
|
|
if (isset($config_xml->fileExtensions)) {
|
2016-06-21 01:30:38 +02:00
|
|
|
$config->file_extensions = [];
|
2016-06-10 20:47:44 +02:00
|
|
|
|
2016-06-21 01:30:38 +02:00
|
|
|
$config->loadFileExtensions($config_xml->fileExtensions->extension);
|
2016-06-10 20:47:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (isset($config_xml->mockClasses) && isset($config_xml->mockClasses->class)) {
|
2016-11-22 01:07:56 +01:00
|
|
|
/** @var \SimpleXMLElement $mock_class */
|
2016-06-10 20:47:44 +02:00
|
|
|
foreach ($config_xml->mockClasses->class as $mock_class) {
|
2018-12-18 05:29:27 +01:00
|
|
|
$config->mock_classes[] = strtolower((string)$mock_class['name']);
|
2016-06-10 00:08:25 +02:00
|
|
|
}
|
2016-06-10 20:47:44 +02:00
|
|
|
}
|
|
|
|
|
2020-10-07 15:56:21 +02:00
|
|
|
if (isset($config_xml->universalObjectCrates) && isset($config_xml->universalObjectCrates->class)) {
|
|
|
|
/** @var \SimpleXMLElement $universal_object_crate */
|
|
|
|
foreach ($config_xml->universalObjectCrates->class as $universal_object_crate) {
|
2020-10-20 23:17:49 +02:00
|
|
|
/** @var string $classString */
|
2020-10-07 15:56:21 +02:00
|
|
|
$classString = $universal_object_crate['name'];
|
|
|
|
$config->addUniversalObjectCrate($classString);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-01 00:40:40 +01:00
|
|
|
if (isset($config_xml->ignoreExceptions)) {
|
|
|
|
if (isset($config_xml->ignoreExceptions->class)) {
|
|
|
|
/** @var \SimpleXMLElement $exception_class */
|
|
|
|
foreach ($config_xml->ignoreExceptions->class as $exception_class) {
|
2019-04-10 16:49:27 +02:00
|
|
|
$exception_name = (string) $exception_class['name'];
|
|
|
|
$global_attribute_text = (string) $exception_class['onlyGlobalScope'];
|
|
|
|
if ($global_attribute_text !== 'true' && $global_attribute_text !== '1') {
|
|
|
|
$config->ignored_exceptions[$exception_name] = true;
|
|
|
|
}
|
|
|
|
$config->ignored_exceptions_in_global_scope[$exception_name] = true;
|
2019-02-01 00:40:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (isset($config_xml->ignoreExceptions->classAndDescendants)) {
|
|
|
|
/** @var \SimpleXMLElement $exception_class */
|
|
|
|
foreach ($config_xml->ignoreExceptions->classAndDescendants as $exception_class) {
|
2019-04-10 16:49:27 +02:00
|
|
|
$exception_name = (string) $exception_class['name'];
|
|
|
|
$global_attribute_text = (string) $exception_class['onlyGlobalScope'];
|
|
|
|
if ($global_attribute_text !== 'true' && $global_attribute_text !== '1') {
|
|
|
|
$config->ignored_exceptions_and_descendants[$exception_name] = true;
|
|
|
|
}
|
|
|
|
$config->ignored_exceptions_and_descendants_in_global_scope[$exception_name] = true;
|
2019-02-01 00:40:40 +01:00
|
|
|
}
|
2018-07-13 05:26:08 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-18 22:42:51 +02:00
|
|
|
if (isset($config_xml->forbiddenFunctions) && isset($config_xml->forbiddenFunctions->function)) {
|
|
|
|
/** @var \SimpleXMLElement $forbidden_function */
|
|
|
|
foreach ($config_xml->forbiddenFunctions->function as $forbidden_function) {
|
|
|
|
$config->forbidden_functions[strtolower((string) $forbidden_function['name'])] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-13 05:26:08 +02:00
|
|
|
if (isset($config_xml->exitFunctions) && isset($config_xml->exitFunctions->function)) {
|
|
|
|
/** @var \SimpleXMLElement $exit_function */
|
|
|
|
foreach ($config_xml->exitFunctions->function as $exit_function) {
|
|
|
|
$config->exit_functions[strtolower((string) $exit_function['name'])] = true;
|
2018-06-22 07:26:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-01 01:21:33 +01:00
|
|
|
if (isset($config_xml->stubs) && isset($config_xml->stubs->file)) {
|
|
|
|
/** @var \SimpleXMLElement $stub_file */
|
|
|
|
foreach ($config_xml->stubs->file as $stub_file) {
|
2020-02-26 22:47:20 +01:00
|
|
|
$stub_file_name = (string)$stub_file['name'];
|
|
|
|
if (!Path::isAbsolute($stub_file_name)) {
|
|
|
|
$stub_file_name = $config->base_dir . DIRECTORY_SEPARATOR . $stub_file_name;
|
|
|
|
}
|
|
|
|
$file_path = realpath($stub_file_name);
|
2017-02-01 01:21:33 +01:00
|
|
|
|
|
|
|
if (!$file_path) {
|
2017-04-28 06:31:55 +02:00
|
|
|
throw new Exception\ConfigException(
|
2018-03-04 01:05:15 +01:00
|
|
|
'Cannot resolve stubfile path ' . $config->base_dir . DIRECTORY_SEPARATOR . $stub_file['name']
|
2017-04-28 06:31:55 +02:00
|
|
|
);
|
2017-02-01 01:21:33 +01:00
|
|
|
}
|
|
|
|
|
2020-10-30 00:41:10 +01:00
|
|
|
if (isset($stub_file['preloadClasses'])) {
|
|
|
|
$preload_classes = (string)$stub_file['preloadClasses'];
|
|
|
|
|
|
|
|
if ($preload_classes === 'true' || $preload_classes === '1') {
|
|
|
|
$config->addPreloadedStubFile($file_path);
|
|
|
|
} else {
|
|
|
|
$config->addStubFile($file_path);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$config->addStubFile($file_path);
|
|
|
|
}
|
2017-02-01 01:21:33 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-18 20:45:55 +02:00
|
|
|
// this plugin loading system borrows heavily from etsy/phan
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
if (isset($config_xml->plugins)) {
|
|
|
|
if (isset($config_xml->plugins->plugin)) {
|
|
|
|
/** @var \SimpleXMLElement $plugin */
|
|
|
|
foreach ($config_xml->plugins->plugin as $plugin) {
|
2019-05-03 16:27:09 +02:00
|
|
|
$plugin_file_name = (string) $plugin['filename'];
|
2016-06-20 06:38:13 +02:00
|
|
|
|
2019-07-08 03:20:12 +02:00
|
|
|
$path = Path::isAbsolute($plugin_file_name)
|
2019-05-03 16:27:09 +02:00
|
|
|
? $plugin_file_name
|
|
|
|
: $config->base_dir . $plugin_file_name;
|
2016-06-20 06:38:13 +02:00
|
|
|
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
$config->addPluginPath($path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (isset($config_xml->plugins->pluginClass)) {
|
|
|
|
/** @var \SimpleXMLElement $plugin */
|
|
|
|
foreach ($config_xml->plugins->pluginClass as $plugin) {
|
|
|
|
$plugin_class_name = $plugin['class'];
|
|
|
|
// any child elements are used as plugin configuration
|
|
|
|
$plugin_config = null;
|
|
|
|
if ($plugin->count()) {
|
|
|
|
$plugin_config = $plugin->children();
|
|
|
|
}
|
|
|
|
|
2020-02-18 16:58:56 +01:00
|
|
|
$config->addPluginClass((string) $plugin_class_name, $plugin_config);
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
}
|
2016-06-18 20:45:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-30 02:07:42 +01:00
|
|
|
if (isset($config_xml->issueHandlers)) {
|
2016-11-22 01:07:56 +01:00
|
|
|
/** @var \SimpleXMLElement $issue_handler */
|
2016-12-30 02:07:42 +01:00
|
|
|
foreach ($config_xml->issueHandlers->children() as $key => $issue_handler) {
|
2019-01-07 14:38:56 +01:00
|
|
|
if ($key === 'PluginIssue') {
|
|
|
|
$custom_class_name = (string) $issue_handler['name'];
|
|
|
|
/** @var string $key */
|
|
|
|
$config->issue_handlers[$custom_class_name] = IssueHandler::loadFromXMLElement(
|
|
|
|
$issue_handler,
|
|
|
|
$base_dir
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
/** @var string $key */
|
|
|
|
$config->issue_handlers[$key] = IssueHandler::loadFromXMLElement(
|
|
|
|
$issue_handler,
|
|
|
|
$base_dir
|
|
|
|
);
|
|
|
|
}
|
2016-06-10 00:08:25 +02:00
|
|
|
}
|
|
|
|
}
|
2016-06-26 19:45:20 +02:00
|
|
|
|
2019-03-06 00:08:41 +01:00
|
|
|
if (isset($config_xml->globals) && isset($config_xml->globals->var)) {
|
|
|
|
/** @var \SimpleXMLElement $var */
|
|
|
|
foreach ($config_xml->globals->var as $var) {
|
2019-03-14 01:15:29 +01:00
|
|
|
$config->globals['$' . (string) $var['name']] = (string) $var['type'];
|
2019-03-06 00:08:41 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-26 19:45:20 +02:00
|
|
|
return $config;
|
2016-06-10 00:08:25 +02:00
|
|
|
}
|
|
|
|
|
2020-09-04 22:26:33 +02:00
|
|
|
public static function getInstance(): Config
|
2016-06-06 07:07:50 +02:00
|
|
|
{
|
2018-01-21 19:38:51 +01:00
|
|
|
if (self::$instance) {
|
|
|
|
return self::$instance;
|
2016-06-06 07:07:50 +02:00
|
|
|
}
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
throw new \UnexpectedValueException('No config initialized');
|
2016-06-06 07:07:50 +02:00
|
|
|
}
|
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function setComposerClassLoader(?ClassLoader $loader = null): void
|
2018-03-03 21:19:05 +01:00
|
|
|
{
|
|
|
|
$this->composer_class_loader = $loader;
|
|
|
|
}
|
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function setCustomErrorLevel(string $issue_key, string $error_level): void
|
2016-12-09 04:37:59 +01:00
|
|
|
{
|
2016-12-30 02:07:42 +01:00
|
|
|
$this->issue_handlers[$issue_key] = new IssueHandler();
|
|
|
|
$this->issue_handlers[$issue_key]->setErrorLevel($error_level);
|
2016-12-09 04:37:59 +01:00
|
|
|
}
|
|
|
|
|
2016-10-30 17:46:18 +01:00
|
|
|
/**
|
2017-05-27 02:16:18 +02:00
|
|
|
* @throws ConfigException if a Config file could not be found
|
|
|
|
*
|
2016-10-30 17:46:18 +01:00
|
|
|
*/
|
2020-09-12 17:24:05 +02:00
|
|
|
private function loadFileExtensions(SimpleXMLElement $extensions): void
|
2016-06-20 06:38:13 +02:00
|
|
|
{
|
|
|
|
foreach ($extensions as $extension) {
|
2016-11-05 02:14:04 +01:00
|
|
|
$extension_name = preg_replace('/^\.?/', '', (string)$extension['name']);
|
2016-06-20 06:38:13 +02:00
|
|
|
$this->file_extensions[] = $extension_name;
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
if (isset($extension['scanner'])) {
|
|
|
|
$path = $this->base_dir . (string)$extension['scanner'];
|
|
|
|
|
|
|
|
if (!file_exists($path)) {
|
|
|
|
throw new Exception\ConfigException('Error parsing config: cannot find file ' . $path);
|
|
|
|
}
|
|
|
|
|
2018-02-12 02:56:34 +01:00
|
|
|
$this->filetype_scanner_paths[$extension_name] = $path;
|
2018-01-21 19:38:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (isset($extension['checker'])) {
|
|
|
|
$path = $this->base_dir . (string)$extension['checker'];
|
2016-06-20 06:38:13 +02:00
|
|
|
|
|
|
|
if (!file_exists($path)) {
|
2016-06-20 07:05:44 +02:00
|
|
|
throw new Exception\ConfigException('Error parsing config: cannot find file ' . $path);
|
2016-06-20 06:38:13 +02:00
|
|
|
}
|
|
|
|
|
2018-11-11 18:01:14 +01:00
|
|
|
$this->filetype_analyzer_paths[$extension_name] = $path;
|
2017-01-02 21:31:18 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-06-20 06:38:13 +02:00
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function addPluginPath(string $path): void
|
2018-02-12 02:56:34 +01:00
|
|
|
{
|
|
|
|
if (!file_exists($path)) {
|
|
|
|
throw new \InvalidArgumentException('Cannot find plugin file ' . $path);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->plugin_paths[] = $path;
|
|
|
|
}
|
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function addPluginClass(string $class_name, ?SimpleXMLElement $plugin_config = null): void
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
{
|
|
|
|
$this->plugin_classes[] = ['class' => $class_name, 'config' => $plugin_config];
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @return array<array{class:string, config:?SimpleXmlElement}> */
|
|
|
|
public function getPluginClasses(): array
|
|
|
|
{
|
|
|
|
return $this->plugin_classes;
|
|
|
|
}
|
|
|
|
|
2017-01-02 21:31:18 +01:00
|
|
|
/**
|
|
|
|
* Initialises all the plugins (done once the config is fully loaded)
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-01-02 21:31:18 +01:00
|
|
|
* @psalm-suppress MixedAssignment
|
|
|
|
*/
|
2020-09-12 17:24:05 +02:00
|
|
|
public function initializePlugins(ProjectAnalyzer $project_analyzer): void
|
2017-01-02 21:31:18 +01:00
|
|
|
{
|
2018-11-11 18:01:14 +01:00
|
|
|
$codebase = $project_analyzer->getCodebase();
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
|
2019-11-27 15:18:47 +01:00
|
|
|
$project_analyzer->progress->debug('Initializing plugins...' . PHP_EOL);
|
|
|
|
|
2019-03-01 14:57:10 +01:00
|
|
|
$socket = new PluginRegistrationSocket($this, $codebase);
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
// initialize plugin classes earlier to let them hook into subsequent load process
|
|
|
|
foreach ($this->plugin_classes as $plugin_class_entry) {
|
|
|
|
$plugin_class_name = $plugin_class_entry['class'];
|
|
|
|
$plugin_config = $plugin_class_entry['config'];
|
2019-11-27 15:18:47 +01:00
|
|
|
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
try {
|
2019-06-01 14:53:42 +02:00
|
|
|
// Below will attempt to load plugins from the project directory first.
|
|
|
|
// Failing that, it will use registered autoload chain, which will load
|
|
|
|
// plugins from Psalm directory or phar file. If that fails as well, it
|
|
|
|
// will fall back to project autoloader. It may seem that the last step
|
|
|
|
// will always fail, but it's only true if project uses Composer autoloader
|
|
|
|
if ($this->composer_class_loader
|
|
|
|
&& ($plugin_class_path = $this->composer_class_loader->findFile($plugin_class_name))
|
|
|
|
) {
|
2019-11-27 15:18:47 +01:00
|
|
|
$project_analyzer->progress->debug(
|
2020-09-13 22:40:31 +02:00
|
|
|
'Loading plugin ' . $plugin_class_name . ' via require' . PHP_EOL
|
2019-11-27 15:18:47 +01:00
|
|
|
);
|
|
|
|
|
2020-08-30 17:32:01 +02:00
|
|
|
self::requirePath($plugin_class_path);
|
2018-11-25 16:41:22 +01:00
|
|
|
} else {
|
|
|
|
if (!class_exists($plugin_class_name, true)) {
|
|
|
|
throw new \UnexpectedValueException($plugin_class_name . ' is not a known class');
|
|
|
|
}
|
2018-11-12 18:03:55 +01:00
|
|
|
}
|
|
|
|
|
2018-11-25 16:41:22 +01:00
|
|
|
/**
|
|
|
|
* @psalm-suppress InvalidStringClass
|
2019-07-05 22:24:00 +02:00
|
|
|
*
|
|
|
|
* @var Plugin\PluginEntryPointInterface
|
2018-11-25 16:41:22 +01:00
|
|
|
*/
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
$plugin_object = new $plugin_class_name;
|
|
|
|
$plugin_object($socket, $plugin_config);
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
throw new ConfigException('Failed to load plugin ' . $plugin_class_name, 0, $e);
|
|
|
|
}
|
2019-11-27 15:18:47 +01:00
|
|
|
|
2020-09-13 22:40:31 +02:00
|
|
|
$project_analyzer->progress->debug('Loaded plugin ' . $plugin_class_name . ' successfully' . PHP_EOL);
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
}
|
2018-01-21 19:38:51 +01:00
|
|
|
|
2018-02-12 02:56:34 +01:00
|
|
|
foreach ($this->filetype_scanner_paths as $extension => $path) {
|
2018-11-06 03:57:36 +01:00
|
|
|
$fq_class_name = $this->getPluginClassForPath(
|
|
|
|
$codebase,
|
|
|
|
$path,
|
2019-01-02 12:58:49 +01:00
|
|
|
FileScanner::class
|
2018-11-06 03:57:36 +01:00
|
|
|
);
|
2018-01-21 19:38:51 +01:00
|
|
|
|
2020-08-30 17:32:01 +02:00
|
|
|
self::requirePath($path);
|
2018-11-12 18:03:55 +01:00
|
|
|
|
|
|
|
$this->filetype_scanners[$extension] = $fq_class_name;
|
2018-02-12 02:56:34 +01:00
|
|
|
}
|
2018-01-21 19:38:51 +01:00
|
|
|
|
2018-11-11 18:01:14 +01:00
|
|
|
foreach ($this->filetype_analyzer_paths as $extension => $path) {
|
2018-11-06 03:57:36 +01:00
|
|
|
$fq_class_name = $this->getPluginClassForPath(
|
|
|
|
$codebase,
|
|
|
|
$path,
|
2019-01-02 12:58:49 +01:00
|
|
|
FileAnalyzer::class
|
2018-11-06 03:57:36 +01:00
|
|
|
);
|
2018-02-12 02:56:34 +01:00
|
|
|
|
2020-08-30 17:32:01 +02:00
|
|
|
self::requirePath($path);
|
2018-11-12 18:03:55 +01:00
|
|
|
|
|
|
|
$this->filetype_analyzers[$extension] = $fq_class_name;
|
2018-01-21 19:38:51 +01:00
|
|
|
}
|
|
|
|
|
2018-02-12 02:56:34 +01:00
|
|
|
foreach ($this->plugin_paths as $path) {
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
try {
|
2018-11-06 03:57:36 +01:00
|
|
|
$plugin_object = new FileBasedPluginAdapter($path, $this, $codebase);
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
$plugin_object($socket);
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
throw new ConfigException('Failed to load plugin ' . $path, 0, $e);
|
2018-02-12 02:56:34 +01:00
|
|
|
}
|
2016-06-20 06:38:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-13 22:40:31 +02:00
|
|
|
private static function requirePath(string $path): void
|
2020-08-30 17:32:01 +02:00
|
|
|
{
|
|
|
|
/** @psalm-suppress UnresolvableInclude */
|
|
|
|
require_once($path);
|
|
|
|
}
|
|
|
|
|
2018-02-12 02:56:34 +01:00
|
|
|
/**
|
2019-01-02 12:58:49 +01:00
|
|
|
* @template T
|
|
|
|
*
|
|
|
|
* @param T::class $must_extend
|
2018-02-12 02:56:34 +01:00
|
|
|
*
|
2019-01-02 15:00:45 +01:00
|
|
|
* @return class-string<T>
|
2018-02-12 02:56:34 +01:00
|
|
|
*/
|
2020-09-12 17:24:05 +02:00
|
|
|
private function getPluginClassForPath(Codebase $codebase, string $path, string $must_extend): string
|
2018-02-12 02:56:34 +01:00
|
|
|
{
|
|
|
|
$file_storage = $codebase->createFileStorageForPath($path);
|
2018-02-21 19:54:11 +01:00
|
|
|
$file_to_scan = new FileScanner($path, $this->shortenFileName($path), true);
|
2018-02-12 02:56:34 +01:00
|
|
|
$file_to_scan->scan(
|
|
|
|
$codebase,
|
|
|
|
$file_storage
|
|
|
|
);
|
|
|
|
|
2018-11-06 03:57:36 +01:00
|
|
|
$declared_classes = ClassLikeAnalyzer::getClassesForFile($codebase, $path);
|
2018-02-12 02:56:34 +01:00
|
|
|
|
2019-01-06 23:32:19 +01:00
|
|
|
if (!count($declared_classes)) {
|
2018-02-12 02:56:34 +01:00
|
|
|
throw new \InvalidArgumentException(
|
2019-01-06 23:32:19 +01:00
|
|
|
'Plugins must have at least one class in the file - ' . $path . ' has ' .
|
2018-02-12 02:56:34 +01:00
|
|
|
count($declared_classes)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-02-12 04:49:19 +01:00
|
|
|
$fq_class_name = reset($declared_classes);
|
|
|
|
|
2019-03-17 22:10:51 +01:00
|
|
|
if (!$codebase->classlikes->classExtends(
|
2018-02-12 04:49:19 +01:00
|
|
|
$fq_class_name,
|
2018-02-12 02:56:34 +01:00
|
|
|
$must_extend
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
throw new \InvalidArgumentException(
|
|
|
|
'This plugin must extend ' . $must_extend . ' - ' . $path . ' does not'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-01-02 12:58:49 +01:00
|
|
|
/**
|
2019-01-02 15:00:45 +01:00
|
|
|
* @var class-string<T>
|
2019-01-02 12:58:49 +01:00
|
|
|
*/
|
2018-02-12 04:49:19 +01:00
|
|
|
return $fq_class_name;
|
2018-02-12 02:56:34 +01:00
|
|
|
}
|
|
|
|
|
2020-10-25 21:17:57 +01:00
|
|
|
public function shortenFileName(string $to): string
|
2016-06-10 23:20:04 +02:00
|
|
|
{
|
2020-10-25 21:31:42 +01:00
|
|
|
if (!is_file($to)) {
|
2020-10-25 21:38:58 +01:00
|
|
|
return preg_replace('/^' . preg_quote($this->base_dir, '/') . '/', '', $to);
|
2020-10-25 21:31:42 +01:00
|
|
|
}
|
|
|
|
|
2020-10-25 21:17:57 +01:00
|
|
|
$from = $this->base_dir;
|
|
|
|
|
|
|
|
// some compatibility fixes for Windows paths
|
|
|
|
$from = is_dir($from) ? rtrim($from, '\/') . '/' : $from;
|
|
|
|
$to = is_dir($to) ? rtrim($to, '\/') . '/' : $to;
|
|
|
|
$from = str_replace('\\', '/', $from);
|
|
|
|
$to = str_replace('\\', '/', $to);
|
|
|
|
|
|
|
|
$from = explode('/', $from);
|
|
|
|
$to = explode('/', $to);
|
|
|
|
$relPath = $to;
|
|
|
|
|
|
|
|
foreach ($from as $depth => $dir) {
|
|
|
|
// find first non-matching dir
|
|
|
|
if ($dir === $to[$depth]) {
|
|
|
|
// ignore this directory
|
|
|
|
array_shift($relPath);
|
|
|
|
} else {
|
|
|
|
// get number of remaining dirs to $from
|
|
|
|
$remaining = count($from) - $depth;
|
|
|
|
if ($remaining > 1) {
|
|
|
|
// add traversals up to first matching dir
|
|
|
|
$padLength = (count($relPath) + $remaining - 1) * -1;
|
|
|
|
$relPath = array_pad($relPath, $padLength, '..');
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return implode('/', $relPath);
|
2016-06-10 23:20:04 +02:00
|
|
|
}
|
|
|
|
|
2020-09-07 01:36:47 +02:00
|
|
|
public function reportIssueInFile(string $issue_type, string $file_path): bool
|
2016-06-10 00:08:25 +02:00
|
|
|
{
|
2020-02-19 02:30:37 +01:00
|
|
|
if (($this->show_mixed_issues === false || $this->level > 2)
|
|
|
|
&& in_array($issue_type, self::MIXED_ISSUES, true)
|
|
|
|
) {
|
2020-02-18 23:23:48 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-09-01 16:56:55 +02:00
|
|
|
if ($this->mustBeIgnored($file_path)) {
|
|
|
|
return false;
|
|
|
|
}
|
2018-06-04 06:15:28 +02:00
|
|
|
|
2019-09-01 16:56:55 +02:00
|
|
|
$dependent_files = [strtolower($file_path) => $file_path];
|
2019-04-27 23:38:24 +02:00
|
|
|
|
2019-09-01 16:56:55 +02:00
|
|
|
$project_analyzer = ProjectAnalyzer::getInstance();
|
2017-03-24 23:34:46 +01:00
|
|
|
|
2019-09-01 16:56:55 +02:00
|
|
|
$codebase = $project_analyzer->getCodebase();
|
2018-05-30 18:23:53 +02:00
|
|
|
|
2019-09-01 16:56:55 +02:00
|
|
|
if (!$this->hide_external_errors) {
|
2018-05-30 18:23:53 +02:00
|
|
|
try {
|
|
|
|
$file_storage = $codebase->file_storage_provider->get($file_path);
|
|
|
|
$dependent_files += $file_storage->required_by_file_paths;
|
|
|
|
} catch (\InvalidArgumentException $e) {
|
|
|
|
// do nothing
|
|
|
|
}
|
2019-09-01 16:56:55 +02:00
|
|
|
}
|
2018-05-30 18:23:53 +02:00
|
|
|
|
2019-09-01 16:56:55 +02:00
|
|
|
$any_file_path_matched = false;
|
2018-05-30 18:23:53 +02:00
|
|
|
|
2019-09-01 16:56:55 +02:00
|
|
|
foreach ($dependent_files as $dependent_file_path) {
|
|
|
|
if (((!$project_analyzer->full_run && $codebase->analyzer->canReportIssues($dependent_file_path))
|
|
|
|
|| $project_analyzer->canReportIssues($dependent_file_path))
|
|
|
|
&& ($file_path === $dependent_file_path || !$this->mustBeIgnored($dependent_file_path))
|
|
|
|
) {
|
|
|
|
$any_file_path_matched = true;
|
|
|
|
break;
|
2018-05-30 18:23:53 +02:00
|
|
|
}
|
2019-09-01 16:56:55 +02:00
|
|
|
}
|
2018-05-30 18:23:53 +02:00
|
|
|
|
2019-09-01 16:56:55 +02:00
|
|
|
if (!$any_file_path_matched) {
|
|
|
|
return false;
|
2016-08-05 21:11:20 +02:00
|
|
|
}
|
|
|
|
|
2017-01-16 18:59:09 +01:00
|
|
|
if ($this->getReportingLevelForFile($issue_type, $file_path) === self::REPORT_SUPPRESS) {
|
2017-03-24 23:34:46 +01:00
|
|
|
return false;
|
2016-06-10 20:47:44 +02:00
|
|
|
}
|
2016-06-10 00:08:25 +02:00
|
|
|
|
2017-03-24 23:34:46 +01:00
|
|
|
return true;
|
2016-06-10 00:08:25 +02:00
|
|
|
}
|
|
|
|
|
2020-09-07 01:36:47 +02:00
|
|
|
public function isInProjectDirs(string $file_path): bool
|
2016-09-01 05:12:35 +02:00
|
|
|
{
|
2017-01-02 21:31:18 +01:00
|
|
|
return $this->project_files && $this->project_files->allows($file_path);
|
2016-09-01 05:12:35 +02:00
|
|
|
}
|
|
|
|
|
2020-09-07 01:36:47 +02:00
|
|
|
public function isInExtraDirs(string $file_path): bool
|
2020-06-19 06:13:09 +02:00
|
|
|
{
|
|
|
|
return $this->extra_files && $this->extra_files->allows($file_path);
|
|
|
|
}
|
|
|
|
|
2020-09-07 01:36:47 +02:00
|
|
|
public function mustBeIgnored(string $file_path): bool
|
2018-06-04 06:15:28 +02:00
|
|
|
{
|
|
|
|
return $this->project_files && $this->project_files->forbids($file_path);
|
|
|
|
}
|
|
|
|
|
2020-09-13 22:40:31 +02:00
|
|
|
public function trackTaintsInPath(string $file_path): bool
|
2020-05-26 05:28:11 +02:00
|
|
|
{
|
|
|
|
return !$this->taint_analysis_ignored_files
|
|
|
|
|| $this->taint_analysis_ignored_files->allows($file_path);
|
|
|
|
}
|
|
|
|
|
2020-09-13 22:40:31 +02:00
|
|
|
public function getReportingLevelForIssue(CodeIssue $e): string
|
2019-05-03 23:12:20 +02:00
|
|
|
{
|
|
|
|
$fqcn_parts = explode('\\', get_class($e));
|
|
|
|
$issue_type = array_pop($fqcn_parts);
|
|
|
|
|
|
|
|
$reporting_level = null;
|
|
|
|
|
|
|
|
if ($e instanceof ClassIssue) {
|
|
|
|
$reporting_level = $this->getReportingLevelForClass($issue_type, $e->fq_classlike_name);
|
|
|
|
} elseif ($e instanceof MethodIssue) {
|
|
|
|
$reporting_level = $this->getReportingLevelForMethod($issue_type, $e->method_id);
|
|
|
|
} elseif ($e instanceof FunctionIssue) {
|
|
|
|
$reporting_level = $this->getReportingLevelForFunction($issue_type, $e->function_id);
|
|
|
|
} elseif ($e instanceof PropertyIssue) {
|
|
|
|
$reporting_level = $this->getReportingLevelForProperty($issue_type, $e->property_id);
|
|
|
|
} elseif ($e instanceof ArgumentIssue && $e->function_id) {
|
|
|
|
$reporting_level = $this->getReportingLevelForArgument($issue_type, $e->function_id);
|
2020-07-16 15:49:59 +02:00
|
|
|
} elseif ($e instanceof VariableIssue) {
|
|
|
|
$reporting_level = $this->getReportingLevelForVariable($issue_type, $e->var_name);
|
2019-05-03 23:12:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($reporting_level === null) {
|
|
|
|
$reporting_level = $this->getReportingLevelForFile($issue_type, $e->getFilePath());
|
|
|
|
}
|
|
|
|
|
2020-09-02 00:09:30 +02:00
|
|
|
if (!$this->report_info && $reporting_level === self::REPORT_INFO) {
|
|
|
|
$reporting_level = self::REPORT_SUPPRESS;
|
|
|
|
}
|
|
|
|
|
2019-05-03 23:12:20 +02:00
|
|
|
$parent_issue_type = self::getParentIssueType($issue_type);
|
|
|
|
|
|
|
|
if ($parent_issue_type && $reporting_level === Config::REPORT_ERROR) {
|
|
|
|
$parent_reporting_level = $this->getReportingLevelForFile($parent_issue_type, $e->getFilePath());
|
|
|
|
|
|
|
|
if ($parent_reporting_level !== $reporting_level) {
|
|
|
|
return $parent_reporting_level;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $reporting_level;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-08-23 19:52:31 +02:00
|
|
|
* @psalm-pure
|
2019-05-03 23:12:20 +02:00
|
|
|
*/
|
2020-09-07 01:36:47 +02:00
|
|
|
public static function getParentIssueType(string $issue_type): ?string
|
2019-05-03 23:12:20 +02:00
|
|
|
{
|
2019-11-11 15:59:56 +01:00
|
|
|
if ($issue_type === 'PossiblyUndefinedIntArrayOffset'
|
|
|
|
|| $issue_type === 'PossiblyUndefinedStringArrayOffset'
|
|
|
|
) {
|
|
|
|
return 'PossiblyUndefinedArrayOffset';
|
|
|
|
}
|
|
|
|
|
2020-02-17 22:33:28 +01:00
|
|
|
if ($issue_type === 'PossiblyNullReference') {
|
|
|
|
return 'NullReference';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($issue_type === 'PossiblyFalseReference') {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($issue_type === 'PossiblyUndefinedArrayOffset') {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-05-03 23:12:20 +02:00
|
|
|
if (strpos($issue_type, 'Possibly') === 0) {
|
|
|
|
$stripped_issue_type = preg_replace('/^Possibly(False|Null)?/', '', $issue_type);
|
|
|
|
|
|
|
|
if (strpos($stripped_issue_type, 'Invalid') === false && strpos($stripped_issue_type, 'Un') !== 0) {
|
|
|
|
$stripped_issue_type = 'Invalid' . $stripped_issue_type;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $stripped_issue_type;
|
|
|
|
}
|
|
|
|
|
2020-11-17 22:08:05 +01:00
|
|
|
if (strpos($issue_type, 'Tainted') === 0) {
|
|
|
|
return 'TaintedInput';
|
|
|
|
}
|
|
|
|
|
2020-02-17 22:33:28 +01:00
|
|
|
if (preg_match('/^(False|Null)[A-Z]/', $issue_type) && !strpos($issue_type, 'Reference')) {
|
2019-05-03 23:12:20 +02:00
|
|
|
return preg_replace('/^(False|Null)/', 'Invalid', $issue_type);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($issue_type === 'UndefinedInterfaceMethod') {
|
|
|
|
return 'UndefinedMethod';
|
|
|
|
}
|
|
|
|
|
2020-01-06 03:07:26 +01:00
|
|
|
if ($issue_type === 'UndefinedMagicPropertyFetch') {
|
|
|
|
return 'UndefinedPropertyFetch';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($issue_type === 'UndefinedMagicPropertyAssignment') {
|
|
|
|
return 'UndefinedPropertyAssignment';
|
|
|
|
}
|
|
|
|
|
2020-01-06 20:24:20 +01:00
|
|
|
if ($issue_type === 'UndefinedMagicMethod') {
|
|
|
|
return 'UndefinedMethod';
|
|
|
|
}
|
|
|
|
|
2019-08-20 18:06:02 +02:00
|
|
|
if ($issue_type === 'PossibleRawObjectIteration') {
|
|
|
|
return 'RawObjectIteration';
|
|
|
|
}
|
|
|
|
|
2019-05-03 23:12:20 +02:00
|
|
|
if ($issue_type === 'UninitializedProperty') {
|
|
|
|
return 'PropertyNotSetInConstructor';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($issue_type === 'InvalidDocblockParamName') {
|
|
|
|
return 'InvalidDocblock';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($issue_type === 'UnusedClosureParam') {
|
|
|
|
return 'UnusedParam';
|
|
|
|
}
|
|
|
|
|
2020-11-22 17:48:05 +01:00
|
|
|
if ($issue_type === 'UnusedConstructor') {
|
|
|
|
return 'UnusedMethod';
|
|
|
|
}
|
|
|
|
|
2019-09-01 23:49:37 +02:00
|
|
|
if ($issue_type === 'StringIncrement') {
|
|
|
|
return 'InvalidOperand';
|
|
|
|
}
|
|
|
|
|
2020-09-11 04:50:47 +02:00
|
|
|
if ($issue_type === 'InvalidLiteralArgument') {
|
|
|
|
return 'InvalidArgument';
|
|
|
|
}
|
|
|
|
|
2019-05-03 23:12:20 +02:00
|
|
|
if ($issue_type === 'TraitMethodSignatureMismatch') {
|
|
|
|
return 'MethodSignatureMismatch';
|
|
|
|
}
|
|
|
|
|
2019-05-14 21:44:46 +02:00
|
|
|
if ($issue_type === 'ImplementedParamTypeMismatch') {
|
|
|
|
return 'MoreSpecificImplementedParamType';
|
|
|
|
}
|
|
|
|
|
2019-05-16 00:41:26 +02:00
|
|
|
if ($issue_type === 'UndefinedDocblockClass') {
|
|
|
|
return 'UndefinedClass';
|
|
|
|
}
|
|
|
|
|
2019-05-03 23:12:20 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-09-07 01:36:47 +02:00
|
|
|
public function getReportingLevelForFile(string $issue_type, string $file_path): string
|
2016-06-21 01:30:38 +02:00
|
|
|
{
|
2016-12-30 02:07:42 +01:00
|
|
|
if (isset($this->issue_handlers[$issue_type])) {
|
2017-01-16 18:59:09 +01:00
|
|
|
return $this->issue_handlers[$issue_type]->getReportingLevelForFile($file_path);
|
2016-06-21 01:30:38 +02:00
|
|
|
}
|
|
|
|
|
2020-02-18 01:06:37 +01:00
|
|
|
// this string is replaced by scoper for Phars, so be careful
|
2020-02-17 22:33:28 +01:00
|
|
|
$issue_class = 'Psalm\\Issue\\' . $issue_type;
|
|
|
|
|
|
|
|
if (!class_exists($issue_class) || !is_a($issue_class, \Psalm\Issue\CodeIssue::class, true)) {
|
|
|
|
return self::REPORT_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @var int */
|
|
|
|
$issue_level = $issue_class::ERROR_LEVEL;
|
|
|
|
|
|
|
|
if ($issue_level > 0 && $issue_level < $this->level) {
|
2020-09-02 00:09:30 +02:00
|
|
|
return self::REPORT_INFO;
|
2020-02-17 22:33:28 +01:00
|
|
|
}
|
|
|
|
|
2016-06-21 01:30:38 +02:00
|
|
|
return self::REPORT_ERROR;
|
|
|
|
}
|
|
|
|
|
2020-09-13 22:39:06 +02:00
|
|
|
public function getReportingLevelForClass(string $issue_type, string $fq_classlike_name): ?string
|
2018-03-21 03:36:03 +01:00
|
|
|
{
|
|
|
|
if (isset($this->issue_handlers[$issue_type])) {
|
|
|
|
return $this->issue_handlers[$issue_type]->getReportingLevelForClass($fq_classlike_name);
|
|
|
|
}
|
2020-09-13 22:39:06 +02:00
|
|
|
|
|
|
|
return null;
|
2018-03-21 03:36:03 +01:00
|
|
|
}
|
|
|
|
|
2020-09-13 22:39:06 +02:00
|
|
|
public function getReportingLevelForMethod(string $issue_type, string $method_id): ?string
|
2018-03-21 03:36:03 +01:00
|
|
|
{
|
|
|
|
if (isset($this->issue_handlers[$issue_type])) {
|
|
|
|
return $this->issue_handlers[$issue_type]->getReportingLevelForMethod($method_id);
|
|
|
|
}
|
2020-09-13 22:39:06 +02:00
|
|
|
|
|
|
|
return null;
|
2018-03-21 03:36:03 +01:00
|
|
|
}
|
|
|
|
|
2020-09-13 22:39:06 +02:00
|
|
|
public function getReportingLevelForFunction(string $issue_type, string $function_id): ?string
|
2019-04-26 00:02:19 +02:00
|
|
|
{
|
|
|
|
if (isset($this->issue_handlers[$issue_type])) {
|
|
|
|
return $this->issue_handlers[$issue_type]->getReportingLevelForFunction($function_id);
|
|
|
|
}
|
2020-09-13 22:39:06 +02:00
|
|
|
|
|
|
|
return null;
|
2019-04-26 00:02:19 +02:00
|
|
|
}
|
|
|
|
|
2020-09-13 22:39:06 +02:00
|
|
|
public function getReportingLevelForArgument(string $issue_type, string $function_id): ?string
|
2019-04-26 00:02:19 +02:00
|
|
|
{
|
|
|
|
if (isset($this->issue_handlers[$issue_type])) {
|
|
|
|
return $this->issue_handlers[$issue_type]->getReportingLevelForArgument($function_id);
|
|
|
|
}
|
2020-09-13 22:39:06 +02:00
|
|
|
|
|
|
|
return null;
|
2019-04-26 00:02:19 +02:00
|
|
|
}
|
|
|
|
|
2020-09-13 22:39:06 +02:00
|
|
|
public function getReportingLevelForProperty(string $issue_type, string $property_id): ?string
|
2018-05-11 06:07:41 +02:00
|
|
|
{
|
|
|
|
if (isset($this->issue_handlers[$issue_type])) {
|
|
|
|
return $this->issue_handlers[$issue_type]->getReportingLevelForProperty($property_id);
|
|
|
|
}
|
2020-09-13 22:39:06 +02:00
|
|
|
|
|
|
|
return null;
|
2018-05-11 06:07:41 +02:00
|
|
|
}
|
|
|
|
|
2020-09-13 22:39:06 +02:00
|
|
|
public function getReportingLevelForVariable(string $issue_type, string $var_name): ?string
|
2020-07-16 15:49:59 +02:00
|
|
|
{
|
|
|
|
if (isset($this->issue_handlers[$issue_type])) {
|
|
|
|
return $this->issue_handlers[$issue_type]->getReportingLevelForVariable($var_name);
|
|
|
|
}
|
2020-09-13 22:39:06 +02:00
|
|
|
|
|
|
|
return null;
|
2020-07-16 15:49:59 +02:00
|
|
|
}
|
|
|
|
|
2016-10-15 06:12:57 +02:00
|
|
|
/**
|
|
|
|
* @return array<string>
|
|
|
|
*/
|
2020-09-04 22:26:33 +02:00
|
|
|
public function getProjectDirectories(): array
|
2016-06-10 00:08:25 +02:00
|
|
|
{
|
2016-12-29 14:42:39 +01:00
|
|
|
if (!$this->project_files) {
|
2016-08-07 02:27:13 +02:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2016-12-29 16:24:10 +01:00
|
|
|
return $this->project_files->getDirectories();
|
2016-06-13 21:33:18 +02:00
|
|
|
}
|
2016-06-10 00:08:25 +02:00
|
|
|
|
2018-05-23 05:38:27 +02:00
|
|
|
/**
|
|
|
|
* @return array<string>
|
|
|
|
*/
|
2020-09-04 22:26:33 +02:00
|
|
|
public function getProjectFiles(): array
|
2018-05-23 05:38:27 +02:00
|
|
|
{
|
|
|
|
if (!$this->project_files) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->project_files->getFiles();
|
|
|
|
}
|
|
|
|
|
2020-06-19 06:13:09 +02:00
|
|
|
/**
|
|
|
|
* @return array<string>
|
|
|
|
*/
|
2020-09-04 22:26:33 +02:00
|
|
|
public function getExtraDirectories(): array
|
2020-06-19 06:13:09 +02:00
|
|
|
{
|
|
|
|
if (!$this->extra_files) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->extra_files->getDirectories();
|
|
|
|
}
|
|
|
|
|
2020-09-07 01:36:47 +02:00
|
|
|
public function reportTypeStatsForFile(string $file_path): bool
|
2018-08-24 23:46:13 +02:00
|
|
|
{
|
2019-03-23 14:50:47 +01:00
|
|
|
return $this->project_files
|
|
|
|
&& $this->project_files->allows($file_path)
|
|
|
|
&& $this->project_files->reportTypeStats($file_path);
|
2018-08-24 23:46:13 +02:00
|
|
|
}
|
|
|
|
|
2020-09-07 01:36:47 +02:00
|
|
|
public function useStrictTypesForFile(string $file_path): bool
|
2018-08-29 21:10:56 +02:00
|
|
|
{
|
|
|
|
return $this->project_files && $this->project_files->useStrictTypes($file_path);
|
|
|
|
}
|
|
|
|
|
2016-10-15 06:12:57 +02:00
|
|
|
/**
|
2020-10-17 18:36:44 +02:00
|
|
|
* @return array<int, string>
|
2016-10-15 06:12:57 +02:00
|
|
|
*/
|
2020-09-04 22:26:33 +02:00
|
|
|
public function getFileExtensions(): array
|
2016-06-13 21:33:18 +02:00
|
|
|
{
|
|
|
|
return $this->file_extensions;
|
2016-06-10 00:08:25 +02:00
|
|
|
}
|
2016-06-10 20:47:44 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
2019-01-02 15:00:45 +01:00
|
|
|
* @return array<string, class-string<FileScanner>>
|
2016-11-02 07:29:00 +01:00
|
|
|
*/
|
2020-09-04 22:26:33 +02:00
|
|
|
public function getFiletypeScanners(): array
|
2018-01-21 19:38:51 +01:00
|
|
|
{
|
|
|
|
return $this->filetype_scanners;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-01-02 15:00:45 +01:00
|
|
|
* @return array<string, class-string<FileAnalyzer>>
|
2018-01-21 19:38:51 +01:00
|
|
|
*/
|
2020-09-04 22:26:33 +02:00
|
|
|
public function getFiletypeAnalyzers(): array
|
2016-06-20 06:38:13 +02:00
|
|
|
{
|
2018-11-11 18:01:14 +01:00
|
|
|
return $this->filetype_analyzers;
|
2016-06-20 06:38:13 +02:00
|
|
|
}
|
|
|
|
|
2016-10-09 23:55:21 +02:00
|
|
|
/**
|
2016-11-01 05:39:41 +01:00
|
|
|
* @return array<int, string>
|
2016-10-09 23:55:21 +02:00
|
|
|
*/
|
2020-09-04 22:26:33 +02:00
|
|
|
public function getMockClasses(): array
|
2016-06-10 20:47:44 +02:00
|
|
|
{
|
|
|
|
return $this->mock_classes;
|
|
|
|
}
|
2016-06-18 20:45:55 +02:00
|
|
|
|
2020-10-30 00:41:10 +01:00
|
|
|
public function visitPreloadedStubFiles(Codebase $codebase, ?Progress $progress = null): void
|
|
|
|
{
|
|
|
|
if ($progress === null) {
|
|
|
|
$progress = new VoidProgress();
|
|
|
|
}
|
|
|
|
|
|
|
|
$core_generic_files = [];
|
|
|
|
|
|
|
|
if (\PHP_VERSION_ID < 80000 && $codebase->php_major_version >= 8) {
|
2020-10-30 18:28:14 +01:00
|
|
|
$stringable_path = dirname(__DIR__, 2) . '/stubs/Php80.php';
|
2020-10-30 00:41:10 +01:00
|
|
|
|
|
|
|
if (!file_exists($stringable_path)) {
|
2020-10-30 18:28:14 +01:00
|
|
|
throw new \UnexpectedValueException('Cannot locate PHP 8.0 classes');
|
2020-10-30 00:41:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$core_generic_files[] = $stringable_path;
|
|
|
|
}
|
|
|
|
|
|
|
|
$stub_files = array_merge($core_generic_files, $this->preloaded_stub_files);
|
|
|
|
|
|
|
|
if (!$stub_files) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($stub_files as $file_path) {
|
|
|
|
$file_path = \str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file_path);
|
|
|
|
$codebase->scanner->addFileToDeepScan($file_path);
|
|
|
|
}
|
|
|
|
|
|
|
|
$progress->debug('Registering preloaded stub files' . "\n");
|
|
|
|
|
|
|
|
$codebase->register_stub_files = true;
|
|
|
|
|
|
|
|
$codebase->scanFiles();
|
|
|
|
|
|
|
|
$codebase->register_stub_files = false;
|
|
|
|
|
|
|
|
$progress->debug('Finished registering preloaded stub files' . "\n");
|
|
|
|
}
|
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function visitStubFiles(Codebase $codebase, ?Progress $progress = null): void
|
2017-02-01 01:21:33 +01:00
|
|
|
{
|
2019-05-30 16:30:41 +02:00
|
|
|
if ($progress === null) {
|
|
|
|
$progress = new VoidProgress();
|
|
|
|
}
|
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
$codebase->register_stub_files = true;
|
2017-02-11 00:12:59 +01:00
|
|
|
|
2018-03-08 23:56:52 +01:00
|
|
|
// note: don't realpath $generic_stubs_path, or phar version will fail
|
2020-10-12 06:59:19 +02:00
|
|
|
$generic_stubs_path = dirname(__DIR__, 2) . '/stubs/CoreGenericFunctions.phpstub';
|
2017-02-11 00:12:59 +01:00
|
|
|
|
2018-03-08 23:56:52 +01:00
|
|
|
if (!file_exists($generic_stubs_path)) {
|
2017-02-11 00:12:59 +01:00
|
|
|
throw new \UnexpectedValueException('Cannot locate core generic stubs');
|
|
|
|
}
|
|
|
|
|
2018-03-08 23:56:52 +01:00
|
|
|
// note: don't realpath $generic_classes_path, or phar version will fail
|
2020-10-12 06:59:19 +02:00
|
|
|
$generic_classes_path = dirname(__DIR__, 2) . '/stubs/CoreGenericClasses.phpstub';
|
2018-02-01 05:27:25 +01:00
|
|
|
|
2018-03-08 23:56:52 +01:00
|
|
|
if (!file_exists($generic_classes_path)) {
|
2018-02-01 05:27:25 +01:00
|
|
|
throw new \UnexpectedValueException('Cannot locate core generic classes');
|
|
|
|
}
|
|
|
|
|
2020-03-10 20:12:55 +01:00
|
|
|
// note: don't realpath $generic_classes_path, or phar version will fail
|
2020-10-12 06:59:19 +02:00
|
|
|
$immutable_classes_path = dirname(__DIR__, 2) . '/stubs/CoreImmutableClasses.phpstub';
|
2020-03-10 20:12:55 +01:00
|
|
|
|
|
|
|
if (!file_exists($immutable_classes_path)) {
|
|
|
|
throw new \UnexpectedValueException('Cannot locate core immutable classes');
|
|
|
|
}
|
|
|
|
|
|
|
|
$core_generic_files = [$generic_stubs_path, $generic_classes_path, $immutable_classes_path];
|
2019-10-18 17:35:24 +02:00
|
|
|
|
2020-10-30 18:28:14 +01:00
|
|
|
if (\PHP_VERSION_ID >= 80000 && $codebase->php_major_version >= 8) {
|
|
|
|
$stringable_path = dirname(__DIR__, 2) . '/stubs/Php80.php';
|
|
|
|
|
|
|
|
if (!file_exists($stringable_path)) {
|
|
|
|
throw new \UnexpectedValueException('Cannot locate PHP 8.0 classes');
|
|
|
|
}
|
|
|
|
|
|
|
|
$core_generic_files[] = $stringable_path;
|
|
|
|
}
|
|
|
|
|
2019-10-18 17:35:24 +02:00
|
|
|
if (\extension_loaded('ds')) {
|
2020-10-12 06:59:19 +02:00
|
|
|
$ext_ds_path = dirname(__DIR__, 2) . '/stubs/ext-ds.php';
|
2019-10-18 17:35:24 +02:00
|
|
|
|
|
|
|
if (!file_exists($ext_ds_path)) {
|
|
|
|
throw new \UnexpectedValueException('Cannot locate core generic classes');
|
|
|
|
}
|
|
|
|
|
|
|
|
$core_generic_files[] = $ext_ds_path;
|
|
|
|
}
|
|
|
|
|
|
|
|
$stub_files = array_merge($core_generic_files, $this->stub_files);
|
2018-02-01 05:27:25 +01:00
|
|
|
|
2020-10-30 20:11:40 +01:00
|
|
|
if ($this->load_xdebug_stub) {
|
|
|
|
$xdebug_stub_path = dirname(__DIR__, 2) . '/stubs/Xdebug.php';
|
|
|
|
|
|
|
|
if (!file_exists($xdebug_stub_path)) {
|
|
|
|
throw new \UnexpectedValueException('Cannot locate XDebug stub');
|
|
|
|
}
|
|
|
|
|
|
|
|
$stub_files[] = $xdebug_stub_path;
|
|
|
|
}
|
|
|
|
|
2019-02-16 17:16:52 +01:00
|
|
|
$phpstorm_meta_path = $this->base_dir . DIRECTORY_SEPARATOR . '.phpstorm.meta.php';
|
|
|
|
|
2020-07-17 16:09:42 +02:00
|
|
|
if ($this->use_phpstorm_meta_path) {
|
|
|
|
if (is_file($phpstorm_meta_path)) {
|
|
|
|
$stub_files[] = $phpstorm_meta_path;
|
|
|
|
} elseif (is_dir($phpstorm_meta_path)) {
|
|
|
|
$phpstorm_meta_path = realpath($phpstorm_meta_path);
|
2020-01-22 17:06:06 +01:00
|
|
|
|
2020-07-17 16:09:42 +02:00
|
|
|
foreach (glob($phpstorm_meta_path . '/*.meta.php', GLOB_NOSORT) as $glob) {
|
|
|
|
if (is_file($glob) && realpath(dirname($glob)) === $phpstorm_meta_path) {
|
|
|
|
$stub_files[] = $glob;
|
|
|
|
}
|
2020-01-22 17:06:06 +01:00
|
|
|
}
|
|
|
|
}
|
2019-02-16 17:16:52 +01:00
|
|
|
}
|
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
foreach ($stub_files as $file_path) {
|
2019-07-21 22:22:04 +02:00
|
|
|
$file_path = \str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file_path);
|
2020-07-08 23:42:51 +02:00
|
|
|
$codebase->scanner->addFileToDeepScan($file_path);
|
2018-06-30 21:29:37 +02:00
|
|
|
}
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
$progress->debug('Registering stub files' . "\n");
|
2018-06-30 21:29:37 +02:00
|
|
|
|
|
|
|
$codebase->scanFiles();
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
$progress->debug('Finished registering stub files' . "\n");
|
2017-02-11 00:12:59 +01:00
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
$codebase->register_stub_files = false;
|
2017-02-01 01:21:33 +01:00
|
|
|
}
|
2020-09-07 22:43:46 +02:00
|
|
|
|
2020-09-04 22:26:33 +02:00
|
|
|
public function getCacheDirectory(): ?string
|
2016-11-04 22:45:12 +01:00
|
|
|
{
|
|
|
|
return $this->cache_directory;
|
|
|
|
}
|
2020-09-07 22:43:46 +02:00
|
|
|
|
2020-09-04 22:26:33 +02:00
|
|
|
public function getGlobalCacheDirectory(): ?string
|
2018-10-15 17:29:57 +02:00
|
|
|
{
|
|
|
|
return $this->global_cache_directory;
|
|
|
|
}
|
|
|
|
|
2016-11-21 05:45:10 +01:00
|
|
|
/**
|
|
|
|
* @return array<string, mixed>
|
|
|
|
*/
|
2020-09-04 22:26:33 +02:00
|
|
|
public function getPredefinedConstants(): array
|
2016-11-21 03:49:06 +01:00
|
|
|
{
|
|
|
|
return $this->predefined_constants;
|
|
|
|
}
|
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function collectPredefinedConstants(): void
|
2016-11-21 03:49:06 +01:00
|
|
|
{
|
|
|
|
$this->predefined_constants = get_defined_constants();
|
|
|
|
}
|
2017-01-16 02:11:02 +01:00
|
|
|
|
|
|
|
/**
|
2019-04-10 00:09:57 +02:00
|
|
|
* @return array<callable-string, bool>
|
2017-01-16 02:11:02 +01:00
|
|
|
*/
|
2020-09-04 22:26:33 +02:00
|
|
|
public function getPredefinedFunctions(): array
|
2017-01-16 02:11:02 +01:00
|
|
|
{
|
|
|
|
return $this->predefined_functions;
|
|
|
|
}
|
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function collectPredefinedFunctions(): void
|
2017-01-16 02:11:02 +01:00
|
|
|
{
|
|
|
|
$defined_functions = get_defined_functions();
|
|
|
|
|
|
|
|
if (isset($defined_functions['user'])) {
|
|
|
|
foreach ($defined_functions['user'] as $function_name) {
|
|
|
|
$this->predefined_functions[$function_name] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isset($defined_functions['internal'])) {
|
|
|
|
foreach ($defined_functions['internal'] as $function_name) {
|
|
|
|
$this->predefined_functions[$function_name] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-12-28 00:56:10 +01:00
|
|
|
|
2020-07-11 23:17:22 +02:00
|
|
|
public function setIncludeCollector(IncludeCollector $include_collector): void
|
|
|
|
{
|
|
|
|
$this->include_collector = $include_collector;
|
|
|
|
}
|
|
|
|
|
2018-01-01 17:47:03 +01:00
|
|
|
/**
|
|
|
|
* @psalm-suppress MixedAssignment
|
|
|
|
* @psalm-suppress MixedArrayAccess
|
|
|
|
*/
|
2020-09-12 17:24:05 +02:00
|
|
|
public function visitComposerAutoloadFiles(ProjectAnalyzer $project_analyzer, ?Progress $progress = null): void
|
2018-01-01 17:47:03 +01:00
|
|
|
{
|
2019-05-30 16:30:41 +02:00
|
|
|
if ($progress === null) {
|
|
|
|
$progress = new VoidProgress();
|
|
|
|
}
|
|
|
|
|
2020-07-11 23:17:22 +02:00
|
|
|
if (!$this->include_collector) {
|
|
|
|
throw new LogicException("IncludeCollector should be set at this point");
|
|
|
|
}
|
|
|
|
|
|
|
|
$vendor_autoload_files_path
|
|
|
|
= $this->base_dir . DIRECTORY_SEPARATOR . 'vendor'
|
|
|
|
. DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'autoload_files.php';
|
|
|
|
|
|
|
|
if (file_exists($vendor_autoload_files_path)) {
|
|
|
|
$this->include_collector->runAndCollect(
|
|
|
|
function () use ($vendor_autoload_files_path) {
|
|
|
|
/**
|
|
|
|
* @psalm-suppress UnresolvableInclude
|
|
|
|
* @var string[]
|
|
|
|
*/
|
|
|
|
return require $vendor_autoload_files_path;
|
|
|
|
}
|
|
|
|
);
|
2018-01-01 17:47:03 +01:00
|
|
|
}
|
|
|
|
|
2020-07-11 23:17:22 +02:00
|
|
|
$codebase = $project_analyzer->getCodebase();
|
2018-01-01 17:47:03 +01:00
|
|
|
|
2020-08-13 15:14:27 +02:00
|
|
|
$this->collectPredefinedFunctions();
|
|
|
|
|
2020-07-11 23:17:22 +02:00
|
|
|
if ($this->autoloader) {
|
|
|
|
// somee classes that we think are missing may not actually be missing
|
|
|
|
// as they might be autoloadable once we require the autoloader below
|
|
|
|
$codebase->classlikes->forgetMissingClassLikes();
|
2018-01-01 17:47:03 +01:00
|
|
|
|
2020-07-11 23:17:22 +02:00
|
|
|
$this->include_collector->runAndCollect(
|
|
|
|
function () {
|
|
|
|
// do this in a separate method so scope does not leak
|
|
|
|
/** @psalm-suppress UnresolvableInclude */
|
|
|
|
require $this->autoloader;
|
2018-01-01 17:47:03 +01:00
|
|
|
}
|
2020-07-11 23:17:22 +02:00
|
|
|
);
|
2018-03-31 02:03:56 +02:00
|
|
|
}
|
|
|
|
|
2020-08-13 15:14:27 +02:00
|
|
|
$this->collectPredefinedConstants();
|
|
|
|
|
2020-07-11 23:17:22 +02:00
|
|
|
$autoload_included_files = $this->include_collector->getFilteredIncludedFiles();
|
2020-05-10 22:01:45 +02:00
|
|
|
|
2020-07-11 23:17:22 +02:00
|
|
|
if ($autoload_included_files) {
|
2018-06-30 21:29:37 +02:00
|
|
|
$codebase->register_autoload_files = true;
|
2018-01-01 17:47:03 +01:00
|
|
|
|
2020-07-11 23:17:22 +02:00
|
|
|
$progress->debug('Registering autoloaded files' . "\n");
|
|
|
|
foreach ($autoload_included_files as $file_path) {
|
2019-07-21 22:22:04 +02:00
|
|
|
$file_path = \str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file_path);
|
2020-07-11 23:17:22 +02:00
|
|
|
$progress->debug(' ' . $file_path . "\n");
|
2018-06-30 21:29:37 +02:00
|
|
|
$codebase->scanner->addFileToDeepScan($file_path);
|
|
|
|
}
|
|
|
|
|
2018-06-29 21:28:45 +02:00
|
|
|
$codebase->scanner->scanFiles($codebase->classlikes);
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
$progress->debug('Finished registering autoloaded files' . "\n");
|
2018-06-30 21:29:37 +02:00
|
|
|
|
2018-11-06 03:57:36 +01:00
|
|
|
$codebase->register_autoload_files = false;
|
2018-01-21 19:38:51 +01:00
|
|
|
}
|
2018-01-01 17:47:03 +01:00
|
|
|
}
|
|
|
|
|
2017-12-28 00:56:10 +01:00
|
|
|
/**
|
2018-03-03 21:19:05 +01:00
|
|
|
* @return string|false
|
2017-12-28 00:56:10 +01:00
|
|
|
*/
|
2020-09-07 01:36:47 +02:00
|
|
|
public function getComposerFilePathForClassLike(string $fq_classlike_name)
|
2017-12-28 00:56:10 +01:00
|
|
|
{
|
2018-03-03 21:19:05 +01:00
|
|
|
if (!$this->composer_class_loader) {
|
2018-03-06 23:30:54 +01:00
|
|
|
return false;
|
2017-12-28 00:56:10 +01:00
|
|
|
}
|
|
|
|
|
2018-03-03 21:19:05 +01:00
|
|
|
return $this->composer_class_loader->findFile($fq_classlike_name);
|
2017-12-28 00:56:10 +01:00
|
|
|
}
|
2017-12-29 18:29:36 +01:00
|
|
|
|
2020-09-13 22:40:31 +02:00
|
|
|
public function getPotentialComposerFilePathForClassLike(string $class): ?string
|
2019-06-05 06:00:42 +02:00
|
|
|
{
|
|
|
|
if (!$this->composer_class_loader) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @var array<string, array<int, string>> */
|
|
|
|
$psr4_prefixes = $this->composer_class_loader->getPrefixesPsr4();
|
|
|
|
|
|
|
|
// PSR-4 lookup
|
|
|
|
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . '.php';
|
|
|
|
|
|
|
|
$candidate_path = null;
|
|
|
|
|
|
|
|
$maxDepth = 0;
|
|
|
|
|
|
|
|
$subPath = $class;
|
|
|
|
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
|
|
|
$subPath = substr($subPath, 0, $lastPos);
|
|
|
|
$search = $subPath . '\\';
|
|
|
|
if (isset($psr4_prefixes[$search])) {
|
|
|
|
$depth = substr_count($search, '\\');
|
|
|
|
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
|
|
|
|
|
|
|
foreach ($psr4_prefixes[$search] as $dir) {
|
|
|
|
$dir = realpath($dir);
|
|
|
|
|
|
|
|
if ($dir
|
|
|
|
&& $depth > $maxDepth
|
|
|
|
&& $this->isInProjectDirs($dir . DIRECTORY_SEPARATOR . 'testdummy.php')
|
|
|
|
) {
|
|
|
|
$maxDepth = $depth;
|
|
|
|
$candidate_path = realpath($dir) . $pathEnd;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $candidate_path;
|
|
|
|
}
|
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public static function removeCacheDirectory(string $dir): void
|
2017-12-29 18:29:36 +01:00
|
|
|
{
|
|
|
|
if (is_dir($dir)) {
|
2020-05-30 23:02:35 +02:00
|
|
|
$objects = scandir($dir, SCANDIR_SORT_NONE);
|
2017-12-29 18:29:36 +01:00
|
|
|
|
|
|
|
if ($objects === false) {
|
|
|
|
throw new \UnexpectedValueException('Not expecting false here');
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($objects as $object) {
|
2020-09-20 00:26:51 +02:00
|
|
|
if ($object !== '.' && $object !== '..') {
|
|
|
|
if (filetype($dir . '/' . $object) === 'dir') {
|
2017-12-29 18:29:36 +01:00
|
|
|
self::removeCacheDirectory($dir . '/' . $object);
|
|
|
|
} else {
|
|
|
|
unlink($dir . '/' . $object);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
reset($objects);
|
|
|
|
rmdir($dir);
|
|
|
|
}
|
|
|
|
}
|
2018-10-18 20:34:46 +02:00
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function setServerMode(): void
|
2018-10-18 20:34:46 +02:00
|
|
|
{
|
|
|
|
$this->cache_directory .= '-s';
|
|
|
|
}
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function addStubFile(string $stub_file): void
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
{
|
2020-06-21 17:43:08 +02:00
|
|
|
$this->stub_files[$stub_file] = $stub_file;
|
|
|
|
}
|
|
|
|
|
2020-09-13 22:40:31 +02:00
|
|
|
public function hasStubFile(string $stub_file): bool
|
2020-06-21 17:43:08 +02:00
|
|
|
{
|
|
|
|
return isset($this->stub_files[$stub_file]);
|
Plugin loading (#855)
* add ability to load plugins by class names
- Plugins need to implement `__invoke(PluginFacade $psalm):void` method
- Plugins are enabled by adding `<pluginClass
class="Qualified\Class\Name"/>`
- `PluginFacade` provides a single point of contact with Psalm, so that
plugins cannot become coupled to Psalm internals
* added `psalm-plugin` cli tool to manage plugins
Available commands:
`psalm-plugin list` - lists available and enabled plugins
`psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`)
`psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`)
Plugin installation:
`composer install plugin-vendor/plugin-package-name`
Plugin authoring:
Plugins are identified by package `type` field, which should contain
`psalm-plugin` string.
`extra.pluginClass` should refer to the name of the class implementing
`__invoke(PluginFacade $psalm):void` function
Todo:
- better config file search
- better output for `psalm-plugin`
- better formatting for modified xml file
- composer skeleton project for plugins
- ability to refer to plugins by package name (cli only)
- composer plugin to (optionally) enable plugin upon installation
- documentation on plugin installation and authoring
- interfaces for plugin dependencies
- interface for plugin entry point
- migration path for legacy plugins
* documented previously undocumented plugin methods
* split legacy plugin registration into a wrapper class
also added `PluginApi` namespace and `RegistrationInterface`
* reuse psalm's config search algorithm
* enable/disable plugins by composer package name
* allow specifying alternative config file name
* whitelist PluginApi namespace
three times, but well, it works now
* interface for plugin entry points
* psalm-plugin as a symfony console app
* fixed errors found by psalm
* suppressed false positive UnusedMethods
* cs fix
* better psalm-plugin output
* don't leave empty `plugins` node to avoid old schema violation
* removed junk file that shouldn't be there
* cs fix
* fixed phpunit failure (constant redefinition)
* work around missing docblock in on symfony console
* php 7.0 compatibility
* allow `pluginClass` child elements as plugin configuration
* decouple console commands from undelying implementation
- introduce PluginListFactory
- add `PluginList::enable(string $class)` and `PluginList::disable(string $class)`
* PluginList tests
* ComposerLock test
* droppped debugging statement
* added part of console command tests
* added tests for EnableCommand
* added DisableCommand tests
* ignore unused args
* ConfigFile test
* disable travis cache in attempt to fix builds
* nah, that didn't work
* update for upstream changes
* rebase fixes
* namespaced `extra` entry for entry point
* s/PluginFacade/PluginRegistrationSocket/g
* Added $config parameter to PluginEntryPointInterface::__invoke()
* cs fixes
* entry point interface php7.0 compatibility
* cleaned up old cruft
- dropped todos I'm not going to pursues
- locked entry point to be a class implementing entry point interface
* fixed legacy plugins docs
* Added RegistrationInterface::registerHooksFromClass()
It mimics the way old plugins were registered in Psalm\Config, so
handler classes extending Psalm\Plugin should be fully compatible with
it.
Since Psalm\Plugin-style plugin registration was moved to
RegistrationSocket, LegacyPlugin now only load file-based plugins, so it
was renamed to FileBasedPluginAdapter.
* Converted EchoChecker plugin to composer-based format
- Its subfolder is registered as a local composer package in the root
composer.json, so it's directly installable with
```
composer require psalm/echo-checker-plugin
```
- Migration is trivial: drop the plugin into a separate folder, then add
simple composer.json and the entry point class.
* Updated docs
* Don't reject hook handlers that inherit handling methods
* strip void return type in stub file
2018-11-11 05:23:36 +01:00
|
|
|
}
|
2019-07-07 14:55:53 +02:00
|
|
|
|
2020-02-26 22:47:20 +01:00
|
|
|
/**
|
2020-06-21 17:43:08 +02:00
|
|
|
* @return array<string, string>
|
2020-02-26 22:47:20 +01:00
|
|
|
*/
|
|
|
|
public function getStubFiles(): array
|
|
|
|
{
|
|
|
|
return $this->stub_files;
|
|
|
|
}
|
|
|
|
|
2020-10-30 00:41:10 +01:00
|
|
|
public function addPreloadedStubFile(string $stub_file): void
|
|
|
|
{
|
|
|
|
$this->preloaded_stub_files[$stub_file] = $stub_file;
|
|
|
|
}
|
|
|
|
|
2020-01-30 08:20:23 +01:00
|
|
|
public function getPhpVersion(): ?string
|
|
|
|
{
|
|
|
|
if (isset($this->configured_php_version)) {
|
|
|
|
return $this->configured_php_version;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->getPHPVersionFromComposerJson();
|
|
|
|
}
|
|
|
|
|
2019-07-07 14:55:53 +02:00
|
|
|
private function setBooleanAttribute(string $name, bool $value): void
|
|
|
|
{
|
|
|
|
$this->$name = $value;
|
|
|
|
}
|
2020-01-30 08:20:23 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @psalm-suppress MixedAssignment
|
|
|
|
* @psalm-suppress MixedArrayAccess
|
|
|
|
*/
|
|
|
|
private function getPHPVersionFromComposerJson(): ?string
|
|
|
|
{
|
2020-10-03 08:26:37 +02:00
|
|
|
$composer_json_path = Composer::getJsonFilePath($this->base_dir);
|
2020-01-30 08:20:23 +01:00
|
|
|
|
|
|
|
if (file_exists($composer_json_path)) {
|
|
|
|
if (!$composer_json = json_decode(file_get_contents($composer_json_path), true)) {
|
|
|
|
throw new \UnexpectedValueException('Invalid composer.json at ' . $composer_json_path);
|
|
|
|
}
|
|
|
|
$php_version = $composer_json['require']['php'] ?? null;
|
|
|
|
|
2020-04-04 17:18:24 +02:00
|
|
|
if (\is_string($php_version)) {
|
2020-01-30 08:20:23 +01:00
|
|
|
foreach (['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0'] as $candidate) {
|
2020-04-04 17:18:24 +02:00
|
|
|
if (Semver::satisfies($candidate, $php_version)) {
|
2020-01-30 08:20:23 +01:00
|
|
|
return $candidate;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
2020-10-07 15:56:21 +02:00
|
|
|
|
|
|
|
public function addUniversalObjectCrate(string $class): void
|
|
|
|
{
|
2020-10-20 23:17:49 +02:00
|
|
|
if (!class_exists($class, true)) {
|
|
|
|
throw new \UnexpectedValueException($class . ' is not a known class');
|
|
|
|
}
|
2020-10-07 15:56:21 +02:00
|
|
|
$this->universal_object_crates[] = $class;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array<int, lowercase-string>
|
|
|
|
*/
|
|
|
|
public function getUniversalObjectCrates(): array
|
|
|
|
{
|
|
|
|
return array_map('strtolower', $this->universal_object_crates);
|
|
|
|
}
|
2016-06-06 07:07:50 +02:00
|
|
|
}
|