mirror of
https://github.com/danog/psalm.git
synced 2024-12-12 01:09:38 +01:00
adef59629a
* swapping phpcs for php-cs-fixer
* workaround for php-cs-fixer treating parenthesis following echo as the function call variant
* amending rules
* blank_line_before_return
* majority of files pass with these disabled, could remove later
* combine_consecutive_unsets
* concat_space
* placeholder for if vimeo/psalm ever goes php:^7.0
* function_to_constant
* disabling include
* linebreak_after_opening_tag, lowercase_cast, magic_constant_casing
* mb_str_functions disabled
* method_separation
* native_function_casing
* native_function_invocations
* new_with_braces disabled to match usage
* no_alias_functions
* no_blank_lines_after_class_opening
* no_blank_lines_after_phpdoc
* no_blank_lines_before_namespace
* no_empty_comment
* no_empty_phpdoc
* no_empty_statement
* no_extra_consecutive_blank_lines
* no_leading_import_slash to discuss
* no_leading_namespace_whitespace
* no_mixed_echo_print
* no_multiline_whitespace_around_double_arrow
* no_multiline_whitespace_before_semicolons
* no_php4_constructor
* no_short_bool_cast
* no_short_echo_tag
* no_singleline_whitespace_before_semicolons
* no_spaces_around_offset
* no_trailing_comma_in_list_call
* no_trailing_comma_in_singleline_array
* no_unneeded_control_parentheses to discuss
* no_unreachable_default_argument_value
* no_unused_imports to discuss
* no_useless_else to discuss
* no_useless_return
* no_whitespace_before_comma_in_array
* no_whitespace_in_blank_line
* non_printable_character
* normalize_index_brace
* ordered_class_elements to discuss
* ordered_imports to discss
* php_unit_construct
* php_unit_dedicate_assert
* php_unit_fqcn_annotation
* php_unit_strict to discuss
* php_unit_test_class_requires_covers to discuss
* phpdoc_add_missing_param_annotation
* phpdoc_align to discuss
* phpdoc_annotation_without_dot to discuss
* phpdoc_indent to discuss
* phpdoc_inline_tag
* phpdoc_no_access
* phpdoc_no_alias_tag
* phpdoc_no_empty_return
* phpdoc_no_package
* phpdoc_no_useless_inheritdoc
* phpdoc_order to discuss
* phpdoc_return_self_reference
* phpdoc_scalar to discuss
* phpdoc_separation to discuss
* phpdoc_single_line_var_spacing
* phpdoc_summary to discuss
* phpdoc_to_comment to discuss
* phpdoc_trim to discuss
* phpdoc_types
* phpdoc_var_without_name
* pow_to_exponentiation
* pre_increment to discuss
* protected_to_private
* psr0 turned off
* psr4 turned on
* random_api_migration
* return_type_declaration to discuss
* self_accessor to discuss
* semicolon_after_instruction
* short_scalar_cast
* silenced_deprecation_error turned off
* simplified_null_return to discuss
* single_quote
* space_after_semicolon
* standardize_not_equals
* strict_comparison to discuss
* strict_param to discuss
* ternary_operator_spaces
* ternary_to_null_coalescing should be set to true if vimeo/psalm ever goes php:^7.0
* trailing_comma_in_multiline_array to discuss
* trim_array_spaces
* unary_operator_spaces
* whitespace_after_comma_in_array to discuss
* multi-version scrutinizer to match travis
* binary_operator_space
* not the best solution, but it works to exclude the call map from php-cs-fixer
* reducing verbosity of config where defaults were used
* dry run php-cs-fixer as part of tests
* disabling rule pending FriendsOfPHP/PHP-CS-Fixer#2739
* enabling no_unused_imports
* enabling ordered_imports
* ignoring user-defined .php_cs
* using $TRAVIS_COMMIT_RANGE to only test modified files
* enabling no_leading_import_slash
* conditionally testing everything
* filter output then perform exact match
* restoring phpcs via partial cherry pick of f65c618
483 lines
13 KiB
PHP
483 lines
13 KiB
PHP
<?php
|
|
namespace Psalm;
|
|
|
|
use Psalm\Checker\FileChecker;
|
|
use Psalm\Type\Union;
|
|
|
|
class Context
|
|
{
|
|
/**
|
|
* @var array<string, Type\Union>
|
|
*/
|
|
public $vars_in_scope = [];
|
|
|
|
/**
|
|
* @var array<string, bool|string>
|
|
*/
|
|
public $vars_possibly_in_scope = [];
|
|
|
|
/**
|
|
* @var boolean
|
|
*/
|
|
public $inside_loop = false;
|
|
|
|
/**
|
|
* Whether or not we're inside the conditional of an if/where etc.
|
|
*
|
|
* This changes whether or not the context is cloned
|
|
*
|
|
* @var boolean
|
|
*/
|
|
public $inside_conditional = false;
|
|
|
|
/**
|
|
* Whether or not we're inside a __construct function
|
|
*
|
|
* @var boolean
|
|
*/
|
|
public $inside_constructor = false;
|
|
|
|
/**
|
|
* @var string|null
|
|
*/
|
|
public $self;
|
|
|
|
/**
|
|
* @var string|null
|
|
*/
|
|
public $parent;
|
|
|
|
/**
|
|
* @var boolean
|
|
*/
|
|
public $check_classes = true;
|
|
|
|
/**
|
|
* @var boolean
|
|
*/
|
|
public $check_variables = true;
|
|
|
|
/**
|
|
* @var boolean
|
|
*/
|
|
public $check_methods = true;
|
|
|
|
/**
|
|
* @var boolean
|
|
*/
|
|
public $check_consts = true;
|
|
|
|
/**
|
|
* @var boolean
|
|
*/
|
|
public $check_functions = true;
|
|
|
|
/**
|
|
* A list of classes checked with class_exists
|
|
*
|
|
* @var array<string,bool>
|
|
*/
|
|
private $phantom_classes = [];
|
|
|
|
/**
|
|
* A list of clauses in Conjunctive Normal Form
|
|
*
|
|
* @var array<int, Clause>
|
|
*/
|
|
public $clauses = [];
|
|
|
|
/**
|
|
* Whether or not to do a deep analysis and collect mutations to this context
|
|
*
|
|
* @var boolean
|
|
*/
|
|
public $collect_mutations = false;
|
|
|
|
/**
|
|
* Whether or not to do a deep analysis and collect initializations from private methods
|
|
*
|
|
* @var boolean
|
|
*/
|
|
public $collect_initializations = false;
|
|
|
|
/**
|
|
* @var array<string, Type\Union>
|
|
*/
|
|
public $constants = [];
|
|
|
|
/**
|
|
* Whether or not to track how many times a variable is used
|
|
*
|
|
* @var boolean
|
|
*/
|
|
public $collect_references = false;
|
|
|
|
/**
|
|
* A list of variables that have been referenced
|
|
*
|
|
* @var array<string, bool>
|
|
*/
|
|
public $referenced_vars = [];
|
|
|
|
/**
|
|
* A list of variables that have been passed by reference (where we know their type)
|
|
*
|
|
* @var array<string, \Psalm\ReferenceConstraint>|null
|
|
*/
|
|
public $byref_constraints;
|
|
|
|
/**
|
|
* If this context inherits from a context, it is here
|
|
*
|
|
* @var Context|null
|
|
*/
|
|
public $parent_context;
|
|
|
|
/**
|
|
* @param string|null $self
|
|
*/
|
|
public function __construct($self = null)
|
|
{
|
|
$this->self = $self;
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function __clone()
|
|
{
|
|
foreach ($this->vars_in_scope as &$type) {
|
|
if ($type) {
|
|
$type = clone $type;
|
|
}
|
|
}
|
|
|
|
foreach ($this->clauses as &$clause) {
|
|
$clause = clone $clause;
|
|
}
|
|
|
|
foreach ($this->constants as &$constant) {
|
|
$constant = clone $constant;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the parent context, looking at the changes within a block and then applying those changes, where
|
|
* necessary, to the parent context
|
|
*
|
|
* @param Context $start_context
|
|
* @param Context $end_context
|
|
* @param bool $has_leaving_statements whether or not the parent scope is abandoned between
|
|
* $start_context and $end_context
|
|
* @param array $vars_to_update
|
|
* @param array $updated_vars
|
|
* @return void
|
|
*/
|
|
public function update(
|
|
Context $start_context,
|
|
Context $end_context,
|
|
$has_leaving_statements,
|
|
array $vars_to_update,
|
|
array &$updated_vars
|
|
) {
|
|
foreach ($this->vars_in_scope as $var => &$context_type) {
|
|
if (isset($start_context->vars_in_scope[$var])) {
|
|
$old_type = $start_context->vars_in_scope[$var];
|
|
|
|
// this is only true if there was some sort of type negation
|
|
if (in_array($var, $vars_to_update)) {
|
|
// if we're leaving, we're effectively deleting the possibility of the if types
|
|
$new_type = !$has_leaving_statements && $end_context->hasVariable($var)
|
|
? $end_context->vars_in_scope[$var]
|
|
: null;
|
|
|
|
// if the type changed within the block of statements, process the replacement
|
|
// also never allow ourselves to remove all types from a union
|
|
if ((string)$old_type !== (string)$new_type && ($new_type || count($context_type->types) > 1)) {
|
|
$context_type->substitute($old_type, $new_type);
|
|
$updated_vars[$var] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Context $original_context
|
|
* @return array<string,Type\Union>
|
|
*/
|
|
public function getRedefinedVars(Context $original_context)
|
|
{
|
|
$redefined_vars = [];
|
|
|
|
foreach ($original_context->vars_in_scope as $var => $context_type) {
|
|
if (isset($this->vars_in_scope[$var]) &&
|
|
!$this->vars_in_scope[$var]->failed_reconciliation &&
|
|
(string)$this->vars_in_scope[$var] !== (string)$context_type
|
|
) {
|
|
$redefined_vars[$var] = $this->vars_in_scope[$var];
|
|
}
|
|
}
|
|
|
|
return $redefined_vars;
|
|
}
|
|
|
|
/**
|
|
* @param Context $original_context
|
|
* @param Context $new_context
|
|
* @return array<int, string>
|
|
*/
|
|
public static function getNewOrUpdatedVarIds(Context $original_context, Context $new_context)
|
|
{
|
|
$redefined_var_ids = [];
|
|
|
|
foreach ($new_context->vars_in_scope as $var_id => $context_type) {
|
|
if (!isset($original_context->vars_in_scope[$var_id]) ||
|
|
(string)$original_context->vars_in_scope[$var_id] !== (string)$context_type
|
|
) {
|
|
$redefined_var_ids[] = $var_id;
|
|
}
|
|
}
|
|
|
|
return $redefined_var_ids;
|
|
}
|
|
|
|
/**
|
|
* @param string $remove_var_id
|
|
* @return void
|
|
*/
|
|
public function remove($remove_var_id)
|
|
{
|
|
unset(
|
|
$this->referenced_vars[$remove_var_id],
|
|
$this->vars_possibly_in_scope[$remove_var_id]
|
|
);
|
|
|
|
if (isset($this->vars_in_scope[$remove_var_id])) {
|
|
$existing_type = $this->vars_in_scope[$remove_var_id];
|
|
unset($this->vars_in_scope[$remove_var_id]);
|
|
|
|
$this->removeDescendents($remove_var_id, $existing_type);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $remove_var_id
|
|
* @param Union|null $new_type
|
|
* @param FileChecker|null $file_checker
|
|
* @return void
|
|
*/
|
|
public function removeVarFromConflictingClauses(
|
|
$remove_var_id,
|
|
Union $new_type = null,
|
|
FileChecker $file_checker = null
|
|
) {
|
|
$clauses_to_keep = [];
|
|
|
|
$new_type_string = (string)$new_type;
|
|
|
|
foreach ($this->clauses as $clause) {
|
|
\Psalm\Checker\AlgebraChecker::calculateNegation($clause);
|
|
|
|
if (!isset($clause->possibilities[$remove_var_id]) ||
|
|
$clause->possibilities[$remove_var_id] === [$new_type_string]
|
|
) {
|
|
$clauses_to_keep[] = $clause;
|
|
} elseif ($file_checker &&
|
|
$new_type &&
|
|
!$new_type->isMixed()
|
|
) {
|
|
$type_changed = false;
|
|
|
|
// if the clause contains any possibilities that would be altered
|
|
foreach ($clause->possibilities[$remove_var_id] as $type) {
|
|
// empty and !empty are not definitive for arrays and scalar types
|
|
if (($type === '!empty' || $type === 'empty') &&
|
|
($new_type->hasArray() || $new_type->hasNumericType())
|
|
) {
|
|
$type_changed = true;
|
|
break;
|
|
}
|
|
|
|
$result_type = \Psalm\Checker\TypeChecker::reconcileTypes(
|
|
$type,
|
|
clone $new_type,
|
|
null,
|
|
$file_checker,
|
|
null,
|
|
[],
|
|
$failed_reconciliation
|
|
);
|
|
|
|
if ((string)$result_type !== $new_type_string) {
|
|
$type_changed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$type_changed) {
|
|
$clauses_to_keep[] = $clause;
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->clauses = $clauses_to_keep;
|
|
|
|
if ($this->parent_context) {
|
|
$this->parent_context->removeVarFromConflictingClauses($remove_var_id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $remove_var_id
|
|
* @param \Psalm\Type\Union|null $existing_type
|
|
* @param \Psalm\Type\Union|null $new_type
|
|
* @param FileChecker|null $file_checker
|
|
* @return void
|
|
*/
|
|
public function removeDescendents(
|
|
$remove_var_id,
|
|
Union $existing_type = null,
|
|
Union $new_type = null,
|
|
FileChecker $file_checker = null
|
|
) {
|
|
if (!$existing_type && isset($this->vars_in_scope[$remove_var_id])) {
|
|
$existing_type = $this->vars_in_scope[$remove_var_id];
|
|
}
|
|
|
|
if (!$existing_type) {
|
|
return;
|
|
}
|
|
|
|
$this->removeVarFromConflictingClauses(
|
|
$remove_var_id,
|
|
$existing_type->isMixed() ? null : $new_type,
|
|
$file_checker
|
|
);
|
|
|
|
if ($existing_type->hasArray() || $existing_type->isMixed()) {
|
|
$vars_to_remove = [];
|
|
|
|
foreach ($this->vars_in_scope as $var_id => $_) {
|
|
if (preg_match('/^' . preg_quote($remove_var_id, DIRECTORY_SEPARATOR) . '[\[\-]/', $var_id)) {
|
|
$vars_to_remove[] = $var_id;
|
|
}
|
|
}
|
|
|
|
foreach ($vars_to_remove as $var_id) {
|
|
unset($this->vars_in_scope[$var_id]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function removeAllObjectVars()
|
|
{
|
|
$vars_to_remove = [];
|
|
|
|
foreach ($this->vars_in_scope as $var_id => $_) {
|
|
if (strpos($var_id, '->') !== false || strpos($var_id, '::') !== false) {
|
|
$vars_to_remove[] = $var_id;
|
|
}
|
|
}
|
|
|
|
if (!$vars_to_remove) {
|
|
return;
|
|
}
|
|
|
|
foreach ($vars_to_remove as $var_id) {
|
|
unset($this->vars_in_scope[$var_id]);
|
|
}
|
|
|
|
$clauses_to_keep = [];
|
|
|
|
foreach ($this->clauses as $clause) {
|
|
$abandon_clause = false;
|
|
|
|
foreach (array_keys($clause->possibilities) as $key) {
|
|
if (strpos($key, '->') !== false || strpos($key, '::') !== false) {
|
|
$abandon_clause = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$abandon_clause) {
|
|
$clauses_to_keep[] = $clause;
|
|
}
|
|
}
|
|
|
|
$this->clauses = $clauses_to_keep;
|
|
|
|
if ($this->parent_context) {
|
|
$this->parent_context->removeAllObjectVars();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Context $op_context
|
|
* @return void
|
|
*/
|
|
public function updateChecks(Context $op_context)
|
|
{
|
|
$this->check_classes = $this->check_classes && $op_context->check_classes;
|
|
$this->check_variables = $this->check_variables && $op_context->check_variables;
|
|
$this->check_methods = $this->check_methods && $op_context->check_methods;
|
|
$this->check_functions = $this->check_functions && $op_context->check_functions;
|
|
$this->check_consts = $this->check_consts && $op_context->check_consts;
|
|
}
|
|
|
|
/**
|
|
* @param string $class_name
|
|
* @return bool
|
|
*/
|
|
public function isPhantomClass($class_name)
|
|
{
|
|
return isset($this->phantom_classes[strtolower($class_name)]);
|
|
}
|
|
|
|
/**
|
|
* @param string $class_name
|
|
* @return void
|
|
*/
|
|
public function addPhantomClass($class_name)
|
|
{
|
|
$this->phantom_classes[strtolower($class_name)] = true;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, bool>
|
|
*/
|
|
public function getPhantomClasses()
|
|
{
|
|
return $this->phantom_classes;
|
|
}
|
|
|
|
/**
|
|
* @param string|null $var_name
|
|
* @return boolean
|
|
*/
|
|
public function hasVariable($var_name)
|
|
{
|
|
if ($this->collect_references) {
|
|
if (!$var_name ||
|
|
(!isset($this->vars_possibly_in_scope[$var_name]) &&
|
|
!isset($this->vars_in_scope[$var_name]))
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
$stripped_var = preg_replace('/(->|\[).*$/', '', $var_name);
|
|
|
|
if ($stripped_var[0] === '$' && $stripped_var !== '$this') {
|
|
$this->referenced_vars[$var_name] = true;
|
|
}
|
|
|
|
return isset($this->vars_in_scope[$var_name]);
|
|
}
|
|
|
|
return $var_name && isset($this->vars_in_scope[$var_name]);
|
|
}
|
|
}
|