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

616 lines
16 KiB
PHP
Raw Normal View History

<?php
2016-07-26 00:37:44 +02:00
namespace Psalm;
2017-09-04 02:52:54 +02:00
use PhpParser;
use Psalm\Checker\StatementsChecker;
2017-09-04 02:52:54 +02:00
use Psalm\Storage\FunctionLikeStorage;
use Psalm\Type\Reconciler;
use Psalm\Type\Union;
2017-04-02 21:26:10 +02:00
class Context
{
/**
* @var array<string, Type\Union>
*/
public $vars_in_scope = [];
/**
2017-12-02 19:32:20 +01:00
* @var array<string, bool>
*/
public $vars_possibly_in_scope = [];
2017-03-13 16:23:26 +01:00
/**
* Whether or not we're inside the conditional of an if/where etc.
*
* This changes whether or not the context is cloned
*
2017-05-27 02:16:18 +02:00
* @var bool
2017-03-13 16:23:26 +01:00
*/
public $inside_conditional = false;
/**
* Whether or not we're inside a __construct function
*
2017-05-27 02:16:18 +02:00
* @var bool
*/
public $inside_constructor = false;
/**
* Whether or not we're inside an isset call
*
* Inside isssets Psalm is more lenient about certain things
*
* @var bool
*/
public $inside_isset = false;
/**
* @var ?CodeLocation
*/
public $include_location = null;
/**
* @var string|null
*/
public $self;
/**
* @var string|null
*/
public $parent;
/**
2017-05-27 02:16:18 +02:00
* @var bool
*/
public $check_classes = true;
/**
2017-05-27 02:16:18 +02:00
* @var bool
*/
public $check_variables = true;
/**
2017-05-27 02:16:18 +02:00
* @var bool
*/
public $check_methods = true;
/**
2017-05-27 02:16:18 +02:00
* @var bool
*/
public $check_consts = true;
/**
2017-05-27 02:16:18 +02:00
* @var bool
*/
public $check_functions = true;
2016-10-22 19:23:18 +02:00
/**
* A list of classes checked with class_exists
2016-11-02 07:29:00 +01:00
*
2016-10-22 19:23:18 +02:00
* @var array<string,bool>
*/
private $phantom_classes = [];
2016-10-22 19:23:18 +02:00
/**
* A list of clauses in Conjunctive Normal Form
*
* @var array<int, Clause>
*/
public $clauses = [];
2017-01-12 03:37:53 +01:00
/**
* Whether or not to do a deep analysis and collect mutations to this context
*
2017-05-27 02:16:18 +02:00
* @var bool
2017-01-12 03:37:53 +01:00
*/
public $collect_mutations = false;
/**
* Whether or not to do a deep analysis and collect initializations from private methods
*
2017-05-27 02:16:18 +02:00
* @var bool
*/
public $collect_initializations = false;
/**
* @var array<string, Type\Union>
*/
public $constants = [];
/**
* Whether or not to track how many times a variable is used
*
2017-05-27 02:16:18 +02:00
* @var bool
*/
public $collect_references = false;
/**
* A list of variables that have been referenced
*
* @var array<string, bool>
*/
public $referenced_var_ids = [];
/**
* 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;
/**
* @var array<string, Type\Union>
*/
public $possible_param_types = [];
/**
* A list of vars that have been assigned to
*
* @var array<string, bool>
*/
2017-11-25 17:21:45 +01:00
public $assigned_var_ids = [];
/**
* @var bool
*/
public $is_global = false;
/**
* @var array<string, bool>
*/
public $protected_var_ids = [];
/**
* @param string|null $self
*/
public function __construct($self = null)
{
$this->self = $self;
}
2016-11-02 07:29:00 +01:00
/**
* @return void
*/
public function __clone()
{
2017-02-02 06:45:23 +01:00
foreach ($this->vars_in_scope as &$type) {
$type = clone $type;
}
foreach ($this->clauses as &$clause) {
$clause = clone $clause;
}
foreach ($this->constants as &$constant) {
$constant = clone $constant;
}
}
/**
2016-11-02 07:29:00 +01:00
* 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
2016-11-02 07:29:00 +01:00
* @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
2017-05-27 02:16:18 +02:00
*
* @return void
*/
2016-11-02 07:29:00 +01:00
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])) {
2016-10-02 17:08:15 +02:00
$old_type = $start_context->vars_in_scope[$var];
// this is only true if there was some sort of type negation
2017-05-27 02:05:57 +02:00
if (in_array($var, $vars_to_update, true)) {
2016-10-02 17:08:15 +02:00
// if we're leaving, we're effectively deleting the possibility of the if types
$new_type = !$has_leaving_statements && $end_context->hasVariable($var)
2016-11-02 07:29:00 +01:00
? $end_context->vars_in_scope[$var]
: null;
2016-10-02 17:08:15 +02:00
// if the type changed within the block of statements, process the replacement
// also never allow ourselves to remove all types from a union
if ((!$new_type || $old_type->getId() !== $new_type->getId())
&& ($new_type || count($context_type->getTypes()) > 1)
) {
2016-10-02 17:08:15 +02:00
$context_type->substitute($old_type, $new_type);
if ($new_type && $new_type->from_docblock) {
$context_type->setFromDocblock();
}
2016-10-02 17:08:15 +02:00
$updated_vars[$var] = true;
}
2016-08-10 07:54:45 +02:00
}
}
}
}
/**
2017-12-03 00:28:18 +01:00
* @param array<string, Type\Union> $vars_in_scope
2017-05-27 02:16:18 +02:00
*
* @return array<string,Type\Union>
*/
2017-12-03 00:28:18 +01:00
public function getRedefinedVars(array $vars_in_scope)
{
$redefined_vars = [];
2017-12-03 00:28:18 +01:00
foreach ($vars_in_scope as $var => $context_type) {
2017-06-29 06:28:37 +02:00
if (!isset($this->vars_in_scope[$var])) {
continue;
}
$this_var = $this->vars_in_scope[$var];
2017-12-03 00:28:18 +01:00
if (!$this_var->failed_reconciliation
&& !$this_var->isEmpty()
&& !$context_type->isEmpty()
&& $this_var->getId() !== $context_type->getId()
2016-11-02 07:29:00 +01:00
) {
2017-06-29 06:28:37 +02:00
$redefined_vars[$var] = $this_var;
}
}
return $redefined_vars;
}
2016-09-17 17:57:44 +02:00
2017-09-04 02:52:54 +02:00
/**
* @return void
*/
public function inferType(
PhpParser\Node\Expr $expr,
FunctionLikeStorage $function_storage,
Type\Union $inferred_type
) {
if (!isset($expr->inferredType)) {
return;
}
$expr_type = $expr->inferredType;
if (($expr_type->isMixed() || $expr_type->getId() === $inferred_type->getId())
2017-09-04 02:52:54 +02:00
&& $expr instanceof PhpParser\Node\Expr\Variable
&& is_string($expr->name)
2017-11-25 17:21:45 +01:00
&& !isset($this->assigned_var_ids['$' . $expr->name])
2017-09-04 02:52:54 +02:00
&& array_key_exists($expr->name, $function_storage->param_types)
&& !$function_storage->param_types[$expr->name]
) {
if (isset($this->possible_param_types[$expr->name])) {
$this->possible_param_types[$expr->name] = Type::combineUnionTypes(
$this->possible_param_types[$expr->name],
$inferred_type
);
} else {
$this->possible_param_types[$expr->name] = $inferred_type;
$this->vars_in_scope['$' . $expr->name] = clone $inferred_type;
}
}
}
/**
* @param Context $original_context
* @param Context $new_context
2017-05-27 02:16:18 +02:00
*
* @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]) ||
$original_context->vars_in_scope[$var_id]->getId() !== $context_type->getId()
) {
$redefined_var_ids[] = $var_id;
}
}
return $redefined_var_ids;
}
2016-10-30 17:46:18 +01:00
/**
* @param string $remove_var_id
2017-05-27 02:16:18 +02:00
*
2016-10-30 17:46:18 +01:00
* @return void
*/
2016-09-17 17:57:44 +02:00
public function remove($remove_var_id)
{
scrutinizer-ci (#152) * 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
2017-05-27 00:26:14 +02:00
unset(
$this->referenced_var_ids[$remove_var_id],
scrutinizer-ci (#152) * 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
2017-05-27 00:26:14 +02:00
$this->vars_possibly_in_scope[$remove_var_id]
);
2016-09-17 17:57:44 +02:00
if (isset($this->vars_in_scope[$remove_var_id])) {
2017-04-02 21:26:10 +02:00
$existing_type = $this->vars_in_scope[$remove_var_id];
2016-09-17 17:57:44 +02:00
unset($this->vars_in_scope[$remove_var_id]);
2017-04-02 21:26:10 +02:00
$this->removeDescendents($remove_var_id, $existing_type);
2016-09-17 17:57:44 +02:00
}
}
/**
* @param string[] $changed_var_ids
2017-05-27 02:16:18 +02:00
*
* @return void
*/
public function removeReconciledClauses(array $changed_var_ids)
{
$this->clauses = array_filter(
$this->clauses,
/** @return bool */
function (Clause $c) use ($changed_var_ids) {
return count($c->possibilities) > 1
|| !in_array(array_keys($c->possibilities)[0], $changed_var_ids, true);
}
);
}
/**
* @param string $remove_var_id
* @param Clause[] $clauses
* @param Union|null $new_type
* @param StatementsChecker|null $statements_checker
*
* @return Clause[]
*/
public static function filterClauses(
2017-04-02 21:26:10 +02:00
$remove_var_id,
array $clauses,
2017-04-02 21:26:10 +02:00
Union $new_type = null,
StatementsChecker $statements_checker = null
2017-04-02 21:26:10 +02:00
) {
$new_type_string = $new_type ? $new_type->getId() : '';
2017-04-02 21:26:10 +02:00
$clauses_to_keep = [];
foreach ($clauses as $clause) {
2017-04-02 21:26:10 +02:00
\Psalm\Checker\AlgebraChecker::calculateNegation($clause);
$quoted_remove_var_id = preg_quote($remove_var_id);
foreach ($clause->possibilities as $var_id => $_) {
if (preg_match('/^' . $quoted_remove_var_id . '[\[\-]/', $var_id)) {
break 2;
}
}
2017-04-02 21:26:10 +02:00
if (!isset($clause->possibilities[$remove_var_id]) ||
$clause->possibilities[$remove_var_id] === [$new_type_string]
) {
$clauses_to_keep[] = $clause;
} elseif ($statements_checker &&
$new_type &&
!$new_type->isMixed()
) {
2017-04-02 21:26:10 +02:00
$type_changed = false;
// if the clause contains any possibilities that would be altered
// by the new type
foreach ($clause->possibilities[$remove_var_id] as $type) {
// empty and !empty are not definitive for arrays and scalar types
if (($type === '!falsy' || $type === 'falsy') &&
($new_type->hasArray() || $new_type->hasNumericType())
) {
$type_changed = true;
break;
}
$result_type = Reconciler::reconcileTypes(
2017-04-02 21:26:10 +02:00
$type,
clone $new_type,
null,
$statements_checker,
2017-04-02 21:26:10 +02:00
null,
[],
$failed_reconciliation
);
if ($result_type->getId() !== $new_type_string) {
2017-04-02 21:26:10 +02:00
$type_changed = true;
break;
}
}
if (!$type_changed) {
$clauses_to_keep[] = $clause;
}
}
}
return $clauses_to_keep;
}
/**
* @param string $remove_var_id
* @param Union|null $new_type
* @param ?StatementsChecker $statements_checker
*
* @return void
*/
public function removeVarFromConflictingClauses(
$remove_var_id,
Union $new_type = null,
StatementsChecker $statements_checker = null
) {
$this->clauses = self::filterClauses($remove_var_id, $this->clauses, $new_type, $statements_checker);
if ($this->parent_context) {
2017-04-02 21:26:10 +02:00
$this->parent_context->removeVarFromConflictingClauses($remove_var_id);
}
}
2016-10-30 17:46:18 +01:00
/**
* @param string $remove_var_id
2017-04-02 21:26:10 +02:00
* @param \Psalm\Type\Union|null $existing_type
* @param \Psalm\Type\Union|null $new_type
* @param ?StatementsChecker $statements_checker
2017-05-27 02:16:18 +02:00
*
2016-10-30 17:46:18 +01:00
* @return void
*/
2017-04-02 21:26:10 +02:00
public function removeDescendents(
$remove_var_id,
Union $existing_type = null,
Union $new_type = null,
StatementsChecker $statements_checker = null
2017-04-02 21:26:10 +02:00
) {
if (!$existing_type && isset($this->vars_in_scope[$remove_var_id])) {
$existing_type = $this->vars_in_scope[$remove_var_id];
2016-09-17 17:57:44 +02:00
}
2017-04-02 21:26:10 +02:00
if (!$existing_type) {
2016-09-17 17:57:44 +02:00
return;
}
if ($this->clauses) {
$this->removeVarFromConflictingClauses(
$remove_var_id,
$existing_type->isMixed()
|| ($new_type && $existing_type->from_docblock !== $new_type->from_docblock)
? null
: $new_type,
$statements_checker
);
}
2017-04-02 21:26:10 +02:00
if ($existing_type->hasArray() || $existing_type->isMixed()) {
2016-09-17 17:57:44 +02:00
$vars_to_remove = [];
2017-02-02 06:45:23 +01:00
foreach ($this->vars_in_scope as $var_id => $_) {
2017-01-18 04:10:21 +01:00
if (preg_match('/^' . preg_quote($remove_var_id, DIRECTORY_SEPARATOR) . '[\[\-]/', $var_id)) {
2016-09-17 17:57:44 +02:00
$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();
}
}
2016-11-02 07:29:00 +01:00
/**
* @param Context $op_context
2017-05-27 02:16:18 +02:00
*
2016-11-02 07:29:00 +01:00
* @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;
}
2016-10-22 19:23:18 +02:00
2016-11-02 07:29:00 +01:00
/**
* @param string $class_name
2017-05-27 02:16:18 +02:00
*
2016-11-02 07:29:00 +01:00
* @return bool
*/
2016-10-22 19:23:18 +02:00
public function isPhantomClass($class_name)
{
return isset($this->phantom_classes[strtolower($class_name)]);
2016-10-22 19:23:18 +02:00
}
2016-11-02 07:29:00 +01:00
/**
* @param string $class_name
2017-05-27 02:16:18 +02:00
*
2016-11-02 07:29:00 +01:00
* @return void
*/
2016-10-22 19:23:18 +02:00
public function addPhantomClass($class_name)
{
$this->phantom_classes[strtolower($class_name)] = true;
}
/**
* @return array<string, bool>
*/
public function getPhantomClasses()
{
return $this->phantom_classes;
2016-10-22 19:23:18 +02:00
}
/**
* @param string|null $var_name
2017-05-27 02:16:18 +02:00
*
* @return bool
*/
public function hasVariable($var_name)
{
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_var_ids[$var_name] = true;
}
return isset($this->vars_in_scope[$var_name]);
}
}