1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-12 09:19:40 +01:00
psalm/src/Psalm/Config.php

465 lines
12 KiB
PHP
Raw Normal View History

2016-06-06 07:07:50 +02:00
<?php
2016-07-26 00:37:44 +02:00
namespace Psalm;
2016-06-06 07:07:50 +02:00
use Psalm\Checker\FileChecker;
2016-11-02 07:29:00 +01:00
use Psalm\Config\FileFilter;
use Psalm\Exception\ConfigException;
2016-06-10 00:08:25 +02:00
use SimpleXMLElement;
2016-06-06 07:07:50 +02:00
class Config
{
2016-07-26 00:39:36 +02:00
const DEFAULT_FILE_NAME = 'psalm.xml';
2016-06-25 00:18:11 +02:00
const REPORT_INFO = 'info';
const REPORT_ERROR = 'error';
2016-06-26 19:45:20 +02:00
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,
2016-06-26 19:45:20 +02:00
self::REPORT_SUPPRESS
2016-06-25 00:18:11 +02:00
];
2016-11-02 07:29:00 +01:00
/**
* @var self|null
*/
protected static $config;
2016-06-10 00:08:25 +02:00
/**
* Whether or not to stop when the first error is seen
2016-11-01 05:39:41 +01:00
*
* @var boolean
*/
public $stop_on_first_error = true;
/**
* Whether or not to use types as defined in docblocks
2016-11-01 05:39:41 +01:00
*
* @var boolean
*/
2016-08-24 06:22:38 +02:00
public $use_docblock_types = true;
2016-06-10 00:08:25 +02:00
/**
* Whether or not to throw an exception on first error
2016-11-01 05:39:41 +01:00
*
* @var boolean
*/
public $throw_exception = false;
/**
2016-11-04 22:45:12 +01:00
* The directory to store PHP Parser (and other) caches
*
* @var string
*/
public $cache_directory = '/var/tmp/psalm';
/**
* Whether or not to use property defaults to inform type when none is listed
*
* @var boolean
*/
public $use_property_default_for_type = true;
2016-11-04 22:45:12 +01:00
/**
* Path to the autoader
2016-11-01 05:39:41 +01:00
*
* @var string|null
*/
public $autoloader;
2016-11-02 07:29:00 +01:00
/**
* @var FileFilter|null
*/
protected $inspect_files;
2016-06-10 00:08:25 +02:00
/**
* The base directory of this config file
2016-11-01 05:39:41 +01:00
*
* @var string
*/
protected $base_dir;
2016-06-06 07:07:50 +02:00
2016-11-02 07:29:00 +01:00
/**
* @var array<int, string>
*/
protected $file_extensions = ['php'];
2016-11-02 07:29:00 +01:00
/**
* @var array<int, string>
*/
protected $filetype_handlers = [];
2016-09-09 22:21:49 +02:00
/**
2016-11-01 05:39:41 +01:00
* @var array<string, FileFilter>
2016-09-09 22:21:49 +02:00
*/
protected $issue_handlers = [];
2016-11-01 05:39:41 +01:00
/**
* @var array<string, string>
*/
protected $custom_error_levels = [];
2016-11-02 07:29:00 +01:00
/**
* @var array<int, string>
*/
protected $mock_classes = [];
2016-06-06 07:07:50 +02:00
2016-11-02 07:29:00 +01:00
/**
* @var boolean
*/
public $hide_external_errors = true;
2016-12-07 00:27:22 +01:00
/** @var bool */
public $allow_includes = true;
2016-06-18 20:45:55 +02:00
/**
2016-07-26 00:37:44 +02:00
* Psalm plugins
2016-11-02 07:29:00 +01:00
*
2016-06-18 20:45:55 +02:00
* @var array<Plugin>
*/
protected $plugins = [];
2016-11-21 03:49:06 +01:00
/** @var array<string, mixed> */
protected $predefined_constants;
2016-06-06 07:07:50 +02:00
private function __construct()
{
2016-11-02 07:29:00 +01:00
self::$config = $this;
2016-06-06 07:07:50 +02:00
}
2016-06-26 19:45:20 +02:00
/**
* Creates a new config object from the file
2016-11-02 07:29:00 +01:00
*
2016-06-26 19:45:20 +02:00
* @param string $file_name
2016-11-01 05:39:41 +01:00
* @return self
2016-11-05 02:14:04 +01:00
* @psalm-suppress MixedArgument
* @psalm-suppress MixedPropertyFetch
* @psalm-suppress MixedMethodCall
2016-06-26 19:45:20 +02:00
*/
public static function loadFromXML($file_name)
2016-06-10 00:08:25 +02:00
{
$file_contents = file_get_contents($file_name);
$config = new self();
$config->base_dir = dirname($file_name) . '/';
2016-06-10 00:08:25 +02:00
$config_xml = new SimpleXMLElement($file_contents);
if (isset($config_xml['stopOnFirstError'])) {
$attribute_text = (string) $config_xml['stopOnFirstError'];
$config->stop_on_first_error = $attribute_text === 'true' || $attribute_text === '1';
2016-06-10 00:08:25 +02:00
}
2016-06-25 00:18:11 +02:00
if (isset($config_xml['useDocblockTypes'])) {
$attribute_text = (string) $config_xml['useDocblockTypes'];
$config->use_docblock_types = $attribute_text === 'true' || $attribute_text === '1';
2016-06-10 00:08:25 +02:00
}
if (isset($config_xml['throwExceptionOnError'])) {
$attribute_text = (string) $config_xml['throwExceptionOnError'];
$config->throw_exception = $attribute_text === 'true' || $attribute_text === '1';
}
if (isset($config_xml['hideExternalErrors'])) {
$attribute_text = (string) $config_xml['hideExternalErrors'];
$config->hide_external_errors = $attribute_text === 'true' || $attribute_text === '1';
}
if (isset($config_xml['autoloader'])) {
$config->autoloader = (string) $config_xml['autoloader'];
}
2016-11-04 22:45:12 +01:00
if (isset($config_xml['cacheDirectory'])) {
2016-11-05 02:14:04 +01:00
$config->cache_directory = (string) $config_xml['cacheDirectory'];
2016-11-04 22:45:12 +01:00
}
if (isset($config_xml['cacheDirectory'])) {
$config->cache_directory = (string) $config_xml['cacheDirectory'];
}
if (isset($config_xml['usePropertyDefaultForType'])) {
$attribute_text = (string) $config_xml['usePropertyDefaultForType'];
$config->use_property_default_for_type = $attribute_text === 'true' || $attribute_text === '1';
}
if (isset($config_xml['allowFileIncludes'])) {
$attribute_text = (string) $config_xml['allowFileIncludes'];
$config->allow_includes = $attribute_text === 'true' || $attribute_text === '1';
}
2016-06-10 00:08:25 +02:00
if (isset($config_xml->inspectFiles)) {
$config->inspect_files = FileFilter::loadFromXML($config_xml->inspectFiles, true);
2016-06-10 00:08:25 +02:00
}
if (isset($config_xml->fileExtensions)) {
$config->file_extensions = [];
$config->loadFileExtensions($config_xml->fileExtensions->extension);
}
if (isset($config_xml->mockClasses) && isset($config_xml->mockClasses->class)) {
/** @var \SimpleXMLElement $mock_class */
foreach ($config_xml->mockClasses->class as $mock_class) {
$config->mock_classes[] = $mock_class['name'];
2016-06-10 00:08:25 +02:00
}
}
2016-06-18 20:45:55 +02:00
// this plugin loading system borrows heavily from etsy/phan
if (isset($config_xml->plugins) && isset($config_xml->plugins->plugin)) {
/** @var \SimpleXMLElement $plugin */
2016-06-18 20:45:55 +02:00
foreach ($config_xml->plugins->plugin as $plugin) {
$plugin_file_name = $plugin['filename'];
$path = $config->base_dir . $plugin_file_name;
if (!file_exists($path)) {
throw new \InvalidArgumentException('Cannot find file ' . $path);
}
$loaded_plugin = require($path);
2016-06-18 20:45:55 +02:00
if (!$loaded_plugin) {
2016-11-02 07:29:00 +01:00
throw new \InvalidArgumentException(
'Plugins must return an instance of that plugin at the end of the file - ' .
$plugin_file_name . ' does not'
);
2016-06-18 20:45:55 +02:00
}
if (!($loaded_plugin instanceof Plugin)) {
2016-11-02 07:29:00 +01:00
throw new \InvalidArgumentException(
'Plugins must extend \Psalm\Plugin - ' . $plugin_file_name . ' does not'
);
2016-06-18 20:45:55 +02:00
}
$config->plugins[] = $loaded_plugin;
2016-06-18 20:45:55 +02:00
}
}
if (isset($config_xml->issueHandler)) {
/** @var \SimpleXMLElement $issue_handler */
foreach ($config_xml->issueHandler->children() as $key => $issue_handler) {
if (isset($issue_handler['errorLevel'])) {
$error_level = (string) $issue_handler['errorLevel'];
2016-06-27 21:10:13 +02:00
if (!in_array($error_level, self::$ERROR_LEVELS)) {
throw new \InvalidArgumentException('Error level ' . $error_level . ' could not be recognised');
}
$config->custom_error_levels[$key] = $error_level;
}
if (isset($issue_handler->excludeFiles)) {
$config->issue_handlers[$key] = FileFilter::loadFromXML($issue_handler->excludeFiles, false);
2016-06-10 00:08:25 +02:00
}
}
}
2016-06-26 19:45:20 +02:00
return $config;
2016-06-10 00:08:25 +02:00
}
/**
* @return $this
*/
2016-06-06 07:07:50 +02:00
public static function getInstance()
{
2016-11-02 07:29:00 +01:00
if (self::$config) {
return self::$config;
2016-06-06 07:07:50 +02:00
}
return new self();
}
2016-10-30 17:46:18 +01:00
/**
* @param array<\SimpleXMLElement> $extensions
* @return void
2016-11-02 07:29:00 +01:00
* @throws ConfigException If a Config file could not be found.
* @psalm-suppress MixedArrayAccess
2016-10-30 17:46:18 +01:00
*/
protected function loadFileExtensions($extensions)
{
foreach ($extensions as $extension) {
2016-11-05 02:14:04 +01:00
$extension_name = preg_replace('/^\.?/', '', (string)$extension['name']);
$this->file_extensions[] = $extension_name;
if (isset($extension['filetypeHandler'])) {
$path = $this->base_dir . $extension['filetypeHandler'];
if (!file_exists($path)) {
2016-06-20 07:05:44 +02:00
throw new Exception\ConfigException('Error parsing config: cannot find file ' . $path);
}
2016-11-21 03:49:06 +01:00
$declared_classes = array_keys(FileChecker::getDeclaredClassesInFile($path));
if (count($declared_classes) !== 1) {
2016-11-02 07:29:00 +01:00
throw new \InvalidArgumentException(
'Filetype handlers must have exactly one class in the file - ' . $path . ' has ' .
count($declared_classes)
);
}
require_once($path);
if (!is_subclass_of($declared_classes[0], 'Psalm\\Checker\\FileChecker')) {
2016-11-02 07:29:00 +01:00
throw new \InvalidArgumentException(
'Filetype handlers must extend \Psalm\Checker\FileChecker - ' . $path . ' does not'
);
}
$this->filetype_handlers[$extension_name] = $declared_classes[0];
}
}
}
2016-10-09 23:55:21 +02:00
/**
* @param string $file_name
* @return string
*/
public function shortenFileName($file_name)
{
return preg_replace('/^' . preg_quote($this->base_dir, '/') . '/', '', $file_name);
}
2016-11-02 07:29:00 +01:00
/**
* @param string $issue_type
* @param string $file_name
* @return bool
*/
2016-06-10 00:08:25 +02:00
public function excludeIssueInFile($issue_type, $file_name)
{
2016-07-12 01:57:39 +02:00
if ($this->getReportingLevel($issue_type) === self::REPORT_SUPPRESS) {
return true;
}
$file_name = $this->shortenFileName($file_name);
if ($this->getIncludeDirs() && $this->hide_external_errors) {
if (!$this->isInProjectDirs($file_name)) {
2016-08-05 21:11:20 +02:00
return true;
}
}
if (!isset($this->issue_handlers[$issue_type])) {
return false;
}
2016-06-10 00:08:25 +02:00
return !$this->issue_handlers[$issue_type]->allows($file_name);
2016-06-10 00:08:25 +02:00
}
2016-11-02 07:29:00 +01:00
/**
* @param string $file_name
* @return bool
*/
public function isInProjectDirs($file_name)
{
foreach ($this->getIncludeDirs() as $dir_name) {
if (preg_match('/^' . preg_quote($dir_name, '/') . '/', $file_name)) {
return true;
}
}
return false;
}
2016-11-02 07:29:00 +01:00
/**
* @param string $issue_type
* @return string
*/
public function getReportingLevel($issue_type)
{
if (isset($this->custom_error_levels[$issue_type])) {
return $this->custom_error_levels[$issue_type];
}
return self::REPORT_ERROR;
}
2016-10-15 06:12:57 +02:00
/**
* @return array<string>
*/
2016-06-13 21:33:18 +02:00
public function getIncludeDirs()
2016-06-10 00:08:25 +02:00
{
2016-08-07 02:27:13 +02:00
if (!$this->inspect_files) {
return [];
}
2016-06-13 21:33:18 +02:00
return $this->inspect_files->getIncludeDirs();
}
2016-06-10 00:08:25 +02:00
2016-10-23 18:24:53 +02:00
/**
* @return string
*/
2016-06-13 21:33:18 +02:00
public function getBaseDir()
{
return $this->base_dir;
}
2016-06-10 00:08:25 +02:00
2016-10-15 06:12:57 +02:00
/**
* @return array<string>
*/
2016-06-13 21:33:18 +02:00
public function getFileExtensions()
{
return $this->file_extensions;
2016-06-10 00:08:25 +02:00
}
2016-11-02 07:29:00 +01:00
/**
* @return array
*/
public function getFiletypeHandlers()
{
return $this->filetype_handlers;
}
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
*/
public function getMockClasses()
{
return $this->mock_classes;
}
2016-06-18 20:45:55 +02:00
2016-11-04 22:45:12 +01:00
/**
2016-11-05 02:14:04 +01:00
* @return string
2016-11-04 22:45:12 +01:00
*/
public function getCacheDirectory()
{
return $this->cache_directory;
}
2016-10-09 23:55:21 +02:00
/**
* @return array<Plugin>
*/
2016-06-18 20:45:55 +02:00
public function getPlugins()
{
return $this->plugins;
}
2016-06-20 22:18:31 +02:00
2016-11-02 07:29:00 +01:00
/**
* @param string $issue_name
* @param FileFilter|null $filter
* @return void
*/
2016-06-20 22:18:31 +02:00
public function setIssueHandler($issue_name, FileFilter $filter = null)
{
$this->issue_handlers[$issue_name] = $filter;
}
2016-11-21 03:49:06 +01:00
2016-11-21 05:45:10 +01:00
/**
* @return array<string, mixed>
*/
2016-11-21 03:49:06 +01:00
public function getPredefinedConstants()
{
return $this->predefined_constants;
}
2016-11-21 05:45:10 +01:00
/**
* @return void
*/
2016-11-21 03:49:06 +01:00
public function collectPredefinedConstants()
{
$this->predefined_constants = get_defined_constants();
}
2016-06-06 07:07:50 +02:00
}