2016-06-20 06:38:13 +02:00
|
|
|
<?php
|
2016-07-26 00:37:44 +02:00
|
|
|
namespace Psalm;
|
2016-06-20 06:38:13 +02:00
|
|
|
|
2021-06-08 04:55:21 +02:00
|
|
|
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
|
|
|
use Psalm\Internal\Clause;
|
|
|
|
use Psalm\Internal\Type\AssertionReconciler;
|
|
|
|
use Psalm\Storage\FunctionLikeStorage;
|
|
|
|
use Psalm\Type\Union;
|
|
|
|
|
2019-07-05 22:24:00 +02:00
|
|
|
use function array_keys;
|
2021-06-08 04:55:21 +02:00
|
|
|
use function array_search;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function count;
|
|
|
|
use function in_array;
|
2021-06-08 04:55:21 +02:00
|
|
|
use function is_int;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function json_encode;
|
|
|
|
use function preg_match;
|
|
|
|
use function preg_quote;
|
|
|
|
use function preg_replace;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function strpos;
|
|
|
|
use function strtolower;
|
2017-04-02 21:26:10 +02:00
|
|
|
|
2016-06-20 06:38:13 +02:00
|
|
|
class Context
|
|
|
|
{
|
2016-10-18 22:14:52 +02:00
|
|
|
/**
|
2017-01-02 06:08:35 +01:00
|
|
|
* @var array<string, Type\Union>
|
2016-10-18 22:14:52 +02:00
|
|
|
*/
|
2016-06-20 06:38:13 +02:00
|
|
|
public $vars_in_scope = [];
|
|
|
|
|
2016-10-18 22:14:52 +02:00
|
|
|
/**
|
2017-12-02 19:32:20 +01:00
|
|
|
* @var array<string, bool>
|
2016-10-18 22:14:52 +02:00
|
|
|
*/
|
2016-06-20 06:38:13 +02:00
|
|
|
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;
|
2016-06-20 22:30:31 +02:00
|
|
|
|
2018-01-08 05:59:17 +01:00
|
|
|
/**
|
|
|
|
* Whether or not we're inside an isset call
|
|
|
|
*
|
2020-07-24 15:34:05 +02:00
|
|
|
* Inside issets Psalm is more lenient about certain things
|
2018-01-08 05:59:17 +01:00
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $inside_isset = false;
|
|
|
|
|
2018-02-10 16:30:08 +01:00
|
|
|
/**
|
2018-04-19 18:16:00 +02:00
|
|
|
* Whether or not we're inside an unset call, where
|
|
|
|
* we don't care about possibly undefined variables
|
2018-02-10 16:30:08 +01:00
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $inside_unset = false;
|
|
|
|
|
2018-04-19 18:16:00 +02:00
|
|
|
/**
|
|
|
|
* Whether or not we're inside an class_exists call, where
|
|
|
|
* we don't care about possibly undefined classes
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $inside_class_exists = false;
|
|
|
|
|
2019-05-07 02:47:55 +02:00
|
|
|
/**
|
|
|
|
* Whether or not we're inside a function/method call
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $inside_call = false;
|
|
|
|
|
2020-09-30 18:28:13 +02:00
|
|
|
/**
|
|
|
|
* Whether or not we're inside any other situation that treats a variable as used
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
2021-06-25 15:54:39 +02:00
|
|
|
public $inside_general_use = false;
|
2020-09-30 18:28:13 +02:00
|
|
|
|
2021-06-10 20:18:15 +02:00
|
|
|
/**
|
|
|
|
* Whether or not we're inside a return expression
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $inside_return = false;
|
|
|
|
|
2020-02-22 04:15:25 +01:00
|
|
|
/**
|
|
|
|
* Whether or not we're inside a throw
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $inside_throw = false;
|
|
|
|
|
2019-08-13 19:15:23 +02:00
|
|
|
/**
|
|
|
|
* Whether or not we're inside an assignment
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $inside_assignment = false;
|
|
|
|
|
2017-06-21 20:22:52 +02:00
|
|
|
/**
|
2018-01-28 22:52:57 +01:00
|
|
|
* @var null|CodeLocation
|
2017-06-21 20:22:52 +02:00
|
|
|
*/
|
|
|
|
public $include_location = null;
|
|
|
|
|
2016-10-18 22:14:52 +02:00
|
|
|
/**
|
|
|
|
* @var string|null
|
|
|
|
*/
|
2016-08-08 17:28:14 +02:00
|
|
|
public $self;
|
|
|
|
|
2016-10-18 22:14:52 +02:00
|
|
|
/**
|
|
|
|
* @var string|null
|
|
|
|
*/
|
2016-08-08 17:28:14 +02:00
|
|
|
public $parent;
|
|
|
|
|
2016-10-18 22:14:52 +02:00
|
|
|
/**
|
2017-05-27 02:16:18 +02:00
|
|
|
* @var bool
|
2016-10-18 22:14:52 +02:00
|
|
|
*/
|
|
|
|
public $check_classes = true;
|
|
|
|
|
|
|
|
/**
|
2017-05-27 02:16:18 +02:00
|
|
|
* @var bool
|
2016-10-18 22:14:52 +02:00
|
|
|
*/
|
|
|
|
public $check_variables = true;
|
|
|
|
|
|
|
|
/**
|
2017-05-27 02:16:18 +02:00
|
|
|
* @var bool
|
2016-10-18 22:14:52 +02:00
|
|
|
*/
|
|
|
|
public $check_methods = true;
|
|
|
|
|
|
|
|
/**
|
2017-05-27 02:16:18 +02:00
|
|
|
* @var bool
|
2016-10-18 22:14:52 +02:00
|
|
|
*/
|
|
|
|
public $check_consts = true;
|
|
|
|
|
|
|
|
/**
|
2017-05-27 02:16:18 +02:00
|
|
|
* @var bool
|
2016-10-18 22:14:52 +02:00
|
|
|
*/
|
|
|
|
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
|
|
|
*
|
2021-03-29 06:11:45 +02:00
|
|
|
* @var array<lowercase-string,true>
|
2016-10-22 19:23:18 +02:00
|
|
|
*/
|
2018-08-10 05:29:30 +02:00
|
|
|
public $phantom_classes = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A list of files checked with file_exists
|
|
|
|
*
|
|
|
|
* @var array<string,bool>
|
|
|
|
*/
|
|
|
|
public $phantom_files = [];
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-12-27 19:58:58 +01:00
|
|
|
/**
|
|
|
|
* A list of clauses in Conjunctive Normal Form
|
|
|
|
*
|
2019-10-09 00:44:46 +02:00
|
|
|
* @var list<Clause>
|
2016-12-27 19:58:58 +01:00
|
|
|
*/
|
|
|
|
public $clauses = [];
|
|
|
|
|
2019-12-08 06:49:34 +01:00
|
|
|
/**
|
|
|
|
* A list of hashed clauses that have already been factored in
|
|
|
|
*
|
2020-08-26 21:35:29 +02:00
|
|
|
* @var list<string|int>
|
2019-12-08 06:49:34 +01:00
|
|
|
*/
|
|
|
|
public $reconciled_expression_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;
|
|
|
|
|
2017-01-27 07:23:12 +01:00
|
|
|
/**
|
2020-04-21 06:04:47 +02:00
|
|
|
* Whether or not to do a deep analysis and collect initializations from private or final methods
|
2017-01-27 07:23:12 +01:00
|
|
|
*
|
2017-05-27 02:16:18 +02:00
|
|
|
* @var bool
|
2017-01-27 07:23:12 +01:00
|
|
|
*/
|
|
|
|
public $collect_initializations = false;
|
|
|
|
|
2020-04-21 06:04:47 +02:00
|
|
|
/**
|
|
|
|
* Whether or not to do a deep analysis and collect initializations from public non-final methods
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $collect_nonprivate_initializations = false;
|
|
|
|
|
2018-01-24 19:11:23 +01:00
|
|
|
/**
|
|
|
|
* Stored to prevent re-analysing methods when checking for initialised properties
|
|
|
|
*
|
|
|
|
* @var array<string, bool>|null
|
|
|
|
*/
|
|
|
|
public $initialized_methods = null;
|
|
|
|
|
2017-01-15 21:58:40 +01:00
|
|
|
/**
|
|
|
|
* @var array<string, Type\Union>
|
|
|
|
*/
|
|
|
|
public $constants = [];
|
|
|
|
|
2018-06-22 07:13:49 +02:00
|
|
|
/**
|
|
|
|
* Whether or not to track exceptions
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $collect_exceptions = false;
|
|
|
|
|
2017-02-01 05:24:33 +01:00
|
|
|
/**
|
|
|
|
* A list of variables that have been referenced
|
|
|
|
*
|
|
|
|
* @var array<string, bool>
|
|
|
|
*/
|
2017-11-24 18:17:28 +01:00
|
|
|
public $referenced_var_ids = [];
|
2017-02-01 05:24:33 +01:00
|
|
|
|
2017-02-23 06:25:28 +01:00
|
|
|
/**
|
|
|
|
* A list of variables that have been passed by reference (where we know their type)
|
|
|
|
*
|
2020-09-30 18:28:13 +02:00
|
|
|
* @var array<string, \Psalm\Internal\ReferenceConstraint>
|
2017-02-23 06:25:28 +01:00
|
|
|
*/
|
2020-09-30 18:28:13 +02:00
|
|
|
public $byref_constraints = [];
|
2017-02-23 06:25:28 +01:00
|
|
|
|
2017-03-18 20:24:14 +01:00
|
|
|
/**
|
|
|
|
* If this context inherits from a context, it is here
|
|
|
|
*
|
|
|
|
* @var Context|null
|
|
|
|
*/
|
|
|
|
public $parent_context;
|
|
|
|
|
2017-09-03 00:15:52 +02:00
|
|
|
/**
|
|
|
|
* @var array<string, Type\Union>
|
|
|
|
*/
|
|
|
|
public $possible_param_types = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A list of vars that have been assigned to
|
|
|
|
*
|
2020-11-01 17:26:42 +01:00
|
|
|
* @var array<string, int>
|
2017-09-03 00:15:52 +02:00
|
|
|
*/
|
2017-11-25 17:21:45 +01:00
|
|
|
public $assigned_var_ids = [];
|
2017-09-03 00:15:52 +02:00
|
|
|
|
2018-05-18 17:02:50 +02:00
|
|
|
/**
|
|
|
|
* A list of vars that have been may have been assigned to
|
|
|
|
*
|
|
|
|
* @var array<string, bool>
|
|
|
|
*/
|
|
|
|
public $possibly_assigned_var_ids = [];
|
|
|
|
|
2018-06-22 07:13:49 +02:00
|
|
|
/**
|
|
|
|
* A list of classes or interfaces that may have been thrown
|
|
|
|
*
|
2019-04-03 01:42:23 +02:00
|
|
|
* @var array<string, array<array-key, CodeLocation>>
|
2018-06-22 07:13:49 +02:00
|
|
|
*/
|
|
|
|
public $possibly_thrown_exceptions = [];
|
|
|
|
|
2017-12-06 06:56:00 +01:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $is_global = false;
|
|
|
|
|
2017-12-17 16:58:03 +01:00
|
|
|
/**
|
|
|
|
* @var array<string, bool>
|
|
|
|
*/
|
|
|
|
public $protected_var_ids = [];
|
|
|
|
|
2018-01-21 22:24:20 +01:00
|
|
|
/**
|
|
|
|
* If we've branched from the main scope, a byte offset for where that branch happened
|
|
|
|
*
|
|
|
|
* @var int|null
|
|
|
|
*/
|
|
|
|
public $branch_point;
|
|
|
|
|
2018-01-24 06:01:08 +01:00
|
|
|
/**
|
2020-01-27 18:17:12 +01:00
|
|
|
* What does break mean in this context?
|
2018-01-24 06:01:08 +01:00
|
|
|
*
|
2020-01-27 18:17:12 +01:00
|
|
|
* 'loop' means we're breaking out of a loop,
|
|
|
|
* 'switch' means we're breaking out of a switch
|
|
|
|
*
|
|
|
|
* @var list<'loop'|'switch'>
|
2018-01-24 06:01:08 +01:00
|
|
|
*/
|
2020-01-27 18:17:12 +01:00
|
|
|
public $break_types = [];
|
2018-01-24 06:01:08 +01:00
|
|
|
|
2018-05-03 19:56:30 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $inside_loop = false;
|
|
|
|
|
2018-06-17 03:54:44 +02:00
|
|
|
/**
|
2018-11-06 03:57:36 +01:00
|
|
|
* @var Internal\Scope\LoopScope|null
|
2018-06-17 03:54:44 +02:00
|
|
|
*/
|
|
|
|
public $loop_scope = null;
|
|
|
|
|
|
|
|
/**
|
2018-11-18 17:39:14 +01:00
|
|
|
* @var Internal\Scope\CaseScope|null
|
2018-06-17 03:54:44 +02:00
|
|
|
*/
|
2018-11-18 17:39:14 +01:00
|
|
|
public $case_scope = null;
|
2018-06-17 03:54:44 +02:00
|
|
|
|
2020-09-21 21:16:19 +02:00
|
|
|
/**
|
|
|
|
* @var Internal\Scope\FinallyScope|null
|
|
|
|
*/
|
|
|
|
public $finally_scope = null;
|
|
|
|
|
2019-12-08 06:49:34 +01:00
|
|
|
/**
|
|
|
|
* @var Context|null
|
|
|
|
*/
|
|
|
|
public $if_context = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var \Psalm\Internal\Scope\IfScope|null
|
|
|
|
*/
|
|
|
|
public $if_scope = null;
|
|
|
|
|
2018-08-28 23:42:39 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $strict_types = false;
|
|
|
|
|
2018-09-26 00:37:24 +02:00
|
|
|
/**
|
|
|
|
* @var string|null
|
|
|
|
*/
|
2019-12-29 00:37:55 +01:00
|
|
|
public $calling_function_id;
|
2018-09-26 00:37:24 +02:00
|
|
|
|
2020-03-26 17:35:27 +01:00
|
|
|
/**
|
|
|
|
* @var lowercase-string|null
|
|
|
|
*/
|
|
|
|
public $calling_method_id;
|
|
|
|
|
2019-02-26 07:03:33 +01:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $inside_negation = false;
|
|
|
|
|
2019-04-20 23:49:49 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $ignore_variable_property = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $ignore_variable_method = false;
|
|
|
|
|
2019-07-18 07:31:48 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $pure = false;
|
|
|
|
|
2019-08-30 18:36:35 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $mutation_free = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $external_mutation_free = false;
|
|
|
|
|
2019-10-22 16:40:37 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $error_suppressing = false;
|
|
|
|
|
2020-03-15 19:31:41 +01:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $has_returned = false;
|
|
|
|
|
2021-07-17 22:00:54 +02:00
|
|
|
/**
|
|
|
|
* @var array<string, bool>
|
|
|
|
*/
|
2021-07-17 22:04:22 +02:00
|
|
|
public $vars_from_global = [];
|
2021-07-17 22:00:54 +02:00
|
|
|
|
2020-09-07 01:36:47 +02:00
|
|
|
public function __construct(?string $self = null)
|
2016-08-14 03:14:32 +02:00
|
|
|
{
|
|
|
|
$this->self = $self;
|
|
|
|
}
|
|
|
|
|
2019-06-30 03:06:21 +02:00
|
|
|
public function __destruct()
|
|
|
|
{
|
|
|
|
$this->case_scope = null;
|
|
|
|
$this->parent_context = null;
|
|
|
|
}
|
|
|
|
|
2016-06-20 06:38:13 +02:00
|
|
|
public function __clone()
|
|
|
|
{
|
2016-12-27 19:58:58 +01:00
|
|
|
foreach ($this->clauses as &$clause) {
|
|
|
|
$clause = clone $clause;
|
|
|
|
}
|
2017-01-15 21:58:40 +01:00
|
|
|
|
|
|
|
foreach ($this->constants as &$constant) {
|
|
|
|
$constant = clone $constant;
|
|
|
|
}
|
2016-06-20 06:38:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
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
|
2016-06-20 06:38:13 +02:00
|
|
|
*
|
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
|
2020-01-22 15:57:59 +01:00
|
|
|
* @param array<string, bool> $updated_vars
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-06-20 06:38:13 +02:00
|
|
|
*/
|
2016-11-02 07:29:00 +01:00
|
|
|
public function update(
|
|
|
|
Context $start_context,
|
|
|
|
Context $end_context,
|
2020-09-07 01:36:47 +02:00
|
|
|
bool $has_leaving_statements,
|
2016-11-02 07:29:00 +01:00
|
|
|
array $vars_to_update,
|
|
|
|
array &$updated_vars
|
2020-09-12 17:24:05 +02:00
|
|
|
): void {
|
2018-02-17 17:24:08 +01:00
|
|
|
foreach ($start_context->vars_in_scope as $var_id => $old_type) {
|
|
|
|
// this is only true if there was some sort of type negation
|
|
|
|
if (in_array($var_id, $vars_to_update, true)) {
|
|
|
|
// if we're leaving, we're effectively deleting the possibility of the if types
|
|
|
|
$new_type = !$has_leaving_statements && $end_context->hasVariable($var_id)
|
|
|
|
? $end_context->vars_in_scope[$var_id]
|
|
|
|
: null;
|
|
|
|
|
|
|
|
$existing_type = isset($this->vars_in_scope[$var_id]) ? $this->vars_in_scope[$var_id] : null;
|
|
|
|
|
|
|
|
if (!$existing_type) {
|
|
|
|
if ($new_type) {
|
|
|
|
$this->vars_in_scope[$var_id] = clone $new_type;
|
|
|
|
$updated_vars[$var_id] = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
2017-12-03 07:24:47 +01:00
|
|
|
|
2019-06-26 06:14:06 +02:00
|
|
|
$existing_type = clone $existing_type;
|
|
|
|
|
2018-02-17 17:24:08 +01:00
|
|
|
// if the type changed within the block of statements, process the replacement
|
|
|
|
// also never allow ourselves to remove all types from a union
|
2018-05-18 17:02:50 +02:00
|
|
|
if ((!$new_type || !$old_type->equals($new_type))
|
2020-01-04 18:20:26 +01:00
|
|
|
&& ($new_type || count($existing_type->getAtomicTypes()) > 1)
|
2018-02-17 17:24:08 +01:00
|
|
|
) {
|
|
|
|
$existing_type->substitute($old_type, $new_type);
|
2017-12-03 07:24:47 +01:00
|
|
|
|
2018-02-17 17:24:08 +01:00
|
|
|
if ($new_type && $new_type->from_docblock) {
|
|
|
|
$existing_type->setFromDocblock();
|
2016-10-02 17:08:15 +02:00
|
|
|
}
|
2018-02-17 17:24:08 +01:00
|
|
|
|
|
|
|
$updated_vars[$var_id] = true;
|
2016-08-10 07:54:45 +02:00
|
|
|
}
|
2019-06-26 06:14:06 +02:00
|
|
|
|
|
|
|
$this->vars_in_scope[$var_id] = $existing_type;
|
2016-06-20 06:38:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-09 23:54:58 +02:00
|
|
|
/**
|
2018-02-17 17:24:08 +01:00
|
|
|
* @param array<string, Type\Union> $new_vars_in_scope
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-10-09 23:54:58 +02:00
|
|
|
* @return array<string,Type\Union>
|
|
|
|
*/
|
2020-10-12 21:46:47 +02:00
|
|
|
public function getRedefinedVars(array $new_vars_in_scope, bool $include_new_vars = false): array
|
2016-06-20 06:38:13 +02:00
|
|
|
{
|
|
|
|
$redefined_vars = [];
|
|
|
|
|
2018-02-17 17:24:08 +01:00
|
|
|
foreach ($this->vars_in_scope as $var_id => $this_type) {
|
|
|
|
if (!isset($new_vars_in_scope[$var_id])) {
|
|
|
|
if ($include_new_vars) {
|
|
|
|
$redefined_vars[$var_id] = $this_type;
|
|
|
|
}
|
2017-06-29 06:28:37 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-02-17 17:24:08 +01:00
|
|
|
$new_type = $new_vars_in_scope[$var_id];
|
2017-06-29 06:28:37 +02:00
|
|
|
|
2021-01-13 17:35:44 +01:00
|
|
|
if (!$this_type->equals($new_type)) {
|
2018-02-17 17:24:08 +01:00
|
|
|
$redefined_vars[$var_id] = $this_type;
|
2016-06-20 06:38:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $redefined_vars;
|
|
|
|
}
|
2016-09-17 17:57:44 +02:00
|
|
|
|
2017-03-15 01:14:25 +01:00
|
|
|
/**
|
2020-10-17 18:36:44 +02:00
|
|
|
* @return list<string>
|
2017-03-15 01:14:25 +01:00
|
|
|
*/
|
2020-09-04 22:26:33 +02:00
|
|
|
public static function getNewOrUpdatedVarIds(Context $original_context, Context $new_context): array
|
2017-03-15 01:14:25 +01:00
|
|
|
{
|
|
|
|
$redefined_var_ids = [];
|
|
|
|
|
|
|
|
foreach ($new_context->vars_in_scope as $var_id => $context_type) {
|
2018-03-17 22:35:36 +01:00
|
|
|
if (!isset($original_context->vars_in_scope[$var_id])
|
2020-11-01 17:26:42 +01:00
|
|
|
|| ($original_context->assigned_var_ids[$var_id] ?? 0)
|
|
|
|
!== ($new_context->assigned_var_ids[$var_id] ?? 0)
|
2018-05-18 17:02:50 +02:00
|
|
|
|| !$original_context->vars_in_scope[$var_id]->equals($context_type)
|
2017-03-15 01:14:25 +01:00
|
|
|
) {
|
|
|
|
$redefined_var_ids[] = $var_id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $redefined_var_ids;
|
|
|
|
}
|
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function remove(string $remove_var_id): void
|
2016-09-17 17:57:44 +02:00
|
|
|
{
|
2017-05-27 00:26:14 +02:00
|
|
|
unset(
|
2017-11-24 18:17:28 +01:00
|
|
|
$this->referenced_var_ids[$remove_var_id],
|
2017-05-27 00:26:14 +02:00
|
|
|
$this->vars_possibly_in_scope[$remove_var_id]
|
|
|
|
);
|
2017-02-01 05:24:33 +01:00
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-08 06:49:34 +01:00
|
|
|
/**
|
2020-08-26 16:41:47 +02:00
|
|
|
* @param Clause[] $clauses
|
|
|
|
* @param array<string, bool> $changed_var_ids
|
2019-12-08 06:49:34 +01:00
|
|
|
*
|
2020-11-06 18:51:41 +01:00
|
|
|
* @return array{list<Clause>, list<Clause>}
|
2020-08-26 16:41:47 +02:00
|
|
|
*
|
|
|
|
* @psalm-pure
|
2019-12-08 06:49:34 +01:00
|
|
|
*/
|
2020-09-04 22:26:33 +02:00
|
|
|
public static function removeReconciledClauses(array $clauses, array $changed_var_ids): array
|
2019-12-08 06:49:34 +01:00
|
|
|
{
|
|
|
|
$included_clauses = [];
|
|
|
|
$rejected_clauses = [];
|
|
|
|
|
|
|
|
foreach ($clauses as $c) {
|
|
|
|
if ($c->wedge) {
|
|
|
|
$included_clauses[] = $c;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($c->possibilities as $key => $_) {
|
|
|
|
if (isset($changed_var_ids[$key])) {
|
|
|
|
$rejected_clauses[] = $c;
|
|
|
|
continue 2;
|
2019-10-09 00:44:46 +02:00
|
|
|
}
|
2019-12-08 06:49:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$included_clauses[] = $c;
|
|
|
|
}
|
|
|
|
|
|
|
|
return [$included_clauses, $rejected_clauses];
|
2017-11-28 06:46:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param Clause[] $clauses
|
|
|
|
*
|
2019-10-09 00:44:46 +02:00
|
|
|
* @return list<Clause>
|
2017-11-28 06:46:41 +01:00
|
|
|
*/
|
|
|
|
public static function filterClauses(
|
2020-09-07 01:36:47 +02:00
|
|
|
string $remove_var_id,
|
2017-11-28 06:46:41 +01:00
|
|
|
array $clauses,
|
2020-09-07 01:36:47 +02:00
|
|
|
?Union $new_type = null,
|
|
|
|
?StatementsAnalyzer $statements_analyzer = null
|
2020-09-12 17:24:05 +02:00
|
|
|
): array {
|
2017-12-03 18:44:08 +01:00
|
|
|
$new_type_string = $new_type ? $new_type->getId() : '';
|
2017-04-02 21:26:10 +02:00
|
|
|
|
2017-11-28 06:46:41 +01:00
|
|
|
$clauses_to_keep = [];
|
|
|
|
|
|
|
|
foreach ($clauses as $clause) {
|
2020-08-26 16:41:47 +02:00
|
|
|
$clause = $clause->calculateNegation();
|
2017-04-02 21:26:10 +02:00
|
|
|
|
2018-07-10 06:39:33 +02:00
|
|
|
$quoted_remove_var_id = preg_quote($remove_var_id, '/');
|
2017-12-15 23:34:21 +01:00
|
|
|
|
|
|
|
foreach ($clause->possibilities as $var_id => $_) {
|
2018-07-10 06:39:33 +02:00
|
|
|
if (preg_match('/' . $quoted_remove_var_id . '[\]\[\-]/', $var_id)) {
|
2017-12-15 23:34:21 +01:00
|
|
|
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]
|
|
|
|
) {
|
2016-12-28 20:20:16 +01:00
|
|
|
$clauses_to_keep[] = $clause;
|
2018-11-11 18:01:14 +01:00
|
|
|
} elseif ($statements_analyzer &&
|
2017-04-02 22:51:27 +02:00
|
|
|
$new_type &&
|
2018-12-08 19:18:55 +01:00
|
|
|
!$new_type->hasMixed()
|
2017-04-02 22:51:27 +02:00
|
|
|
) {
|
2017-04-02 21:26:10 +02:00
|
|
|
$type_changed = false;
|
|
|
|
|
|
|
|
// if the clause contains any possibilities that would be altered
|
2017-12-02 23:57:58 +01:00
|
|
|
// by the new type
|
2017-04-03 01:06:18 +02:00
|
|
|
foreach ($clause->possibilities[$remove_var_id] as $type) {
|
2019-08-16 16:32:03 +02:00
|
|
|
// if we're negating a type, we generally don't need the clause anymore
|
|
|
|
if ($type[0] === '!' && $type !== '!falsy' && $type !== '!empty') {
|
|
|
|
$type_changed = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2017-04-03 01:06:18 +02:00
|
|
|
// empty and !empty are not definitive for arrays and scalar types
|
2017-10-22 17:57:41 +02:00
|
|
|
if (($type === '!falsy' || $type === 'falsy') &&
|
2018-05-07 07:26:06 +02:00
|
|
|
($new_type->hasArray() || $new_type->hasPossiblyNumericType())
|
2017-04-03 01:06:18 +02:00
|
|
|
) {
|
|
|
|
$type_changed = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2019-08-10 19:22:21 +02:00
|
|
|
$result_type = AssertionReconciler::reconcile(
|
2017-04-02 21:26:10 +02:00
|
|
|
$type,
|
|
|
|
clone $new_type,
|
|
|
|
null,
|
2018-11-11 18:01:14 +01:00
|
|
|
$statements_analyzer,
|
2018-12-08 19:18:55 +01:00
|
|
|
false,
|
2019-01-23 05:42:54 +01:00
|
|
|
[],
|
2017-04-02 21:26:10 +02:00
|
|
|
null,
|
|
|
|
[],
|
|
|
|
$failed_reconciliation
|
|
|
|
);
|
|
|
|
|
2017-12-03 18:44:08 +01:00
|
|
|
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;
|
|
|
|
}
|
2016-12-28 20:20:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-28 06:46:41 +01:00
|
|
|
return $clauses_to_keep;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function removeVarFromConflictingClauses(
|
2020-09-07 01:36:47 +02:00
|
|
|
string $remove_var_id,
|
|
|
|
?Union $new_type = null,
|
|
|
|
?StatementsAnalyzer $statements_analyzer = null
|
2020-09-12 17:24:05 +02:00
|
|
|
): void {
|
2018-11-11 18:01:14 +01:00
|
|
|
$this->clauses = self::filterClauses($remove_var_id, $this->clauses, $new_type, $statements_analyzer);
|
2017-03-18 20:24:14 +01:00
|
|
|
|
|
|
|
if ($this->parent_context) {
|
2017-04-02 21:26:10 +02:00
|
|
|
$this->parent_context->removeVarFromConflictingClauses($remove_var_id);
|
2017-03-18 20:24:14 +01:00
|
|
|
}
|
2016-12-28 20:20:16 +01:00
|
|
|
}
|
|
|
|
|
2021-01-17 20:28:28 +01:00
|
|
|
/**
|
|
|
|
* This method is used after assignments to variables to remove any existing
|
|
|
|
* items in $vars_in_scope that are now made redundant by an update to some data
|
|
|
|
*/
|
2017-04-02 21:26:10 +02:00
|
|
|
public function removeDescendents(
|
2020-09-07 01:36:47 +02:00
|
|
|
string $remove_var_id,
|
|
|
|
?Union $existing_type = null,
|
|
|
|
?Union $new_type = null,
|
|
|
|
?StatementsAnalyzer $statements_analyzer = null
|
2020-10-12 21:02:52 +02:00
|
|
|
): void {
|
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;
|
|
|
|
}
|
|
|
|
|
2020-12-08 22:39:06 +01:00
|
|
|
$existing_type->allow_mutations = true;
|
|
|
|
|
2020-01-11 05:46:45 +01:00
|
|
|
$this->removeVarFromConflictingClauses(
|
|
|
|
$remove_var_id,
|
|
|
|
$existing_type->hasMixed()
|
|
|
|
|| ($new_type && $existing_type->from_docblock !== $new_type->from_docblock)
|
|
|
|
? null
|
|
|
|
: $new_type,
|
|
|
|
$statements_analyzer
|
|
|
|
);
|
2016-12-27 19:58:58 +01:00
|
|
|
|
2020-12-07 00:14:21 +01:00
|
|
|
foreach ($this->vars_in_scope as $var_id => $type) {
|
2018-07-07 01:16:31 +02:00
|
|
|
if (preg_match('/' . preg_quote($remove_var_id, '/') . '[\]\[\-]/', $var_id)) {
|
2020-08-03 08:06:42 +02:00
|
|
|
unset($this->vars_in_scope[$var_id]);
|
2016-09-17 17:57:44 +02:00
|
|
|
}
|
2020-12-07 00:14:21 +01:00
|
|
|
|
|
|
|
foreach ($type->getAtomicTypes() as $atomic_type) {
|
|
|
|
if ($atomic_type instanceof Type\Atomic\DependentType
|
|
|
|
&& $atomic_type->getVarId() === $remove_var_id
|
|
|
|
) {
|
|
|
|
$type->addType($atomic_type->getReplacement());
|
|
|
|
}
|
|
|
|
}
|
2018-02-17 17:24:08 +01:00
|
|
|
}
|
2016-09-17 17:57:44 +02:00
|
|
|
}
|
2016-10-18 22:14:52 +02:00
|
|
|
|
2021-03-05 06:39:19 +01:00
|
|
|
public function removeMutableObjectVars(bool $methods_only = false): void
|
2017-04-15 03:32:14 +02:00
|
|
|
{
|
|
|
|
$vars_to_remove = [];
|
|
|
|
|
2020-12-08 22:39:06 +01:00
|
|
|
foreach ($this->vars_in_scope as $var_id => $type) {
|
|
|
|
if ($type->has_mutations
|
|
|
|
&& (strpos($var_id, '->') !== false || strpos($var_id, '::') !== false)
|
2021-03-05 06:39:19 +01:00
|
|
|
&& (!$methods_only || strpos($var_id, '()'))
|
2020-12-08 22:39:06 +01:00
|
|
|
) {
|
2017-04-15 03:32:14 +02:00
|
|
|
$vars_to_remove[] = $var_id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$vars_to_remove) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($vars_to_remove as $var_id) {
|
2018-04-07 00:28:22 +02:00
|
|
|
unset($this->vars_in_scope[$var_id], $this->vars_possibly_in_scope[$var_id]);
|
2017-04-15 03:32:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$clauses_to_keep = [];
|
|
|
|
|
|
|
|
foreach ($this->clauses as $clause) {
|
|
|
|
$abandon_clause = false;
|
|
|
|
|
|
|
|
foreach (array_keys($clause->possibilities) as $key) {
|
2021-03-05 06:39:19 +01:00
|
|
|
if ((strpos($key, '->') !== false || strpos($key, '::') !== false)
|
|
|
|
&& (!$methods_only || strpos($key, '()'))
|
|
|
|
) {
|
2017-04-15 03:32:14 +02:00
|
|
|
$abandon_clause = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$abandon_clause) {
|
|
|
|
$clauses_to_keep[] = $clause;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->clauses = $clauses_to_keep;
|
2021-02-26 00:11:58 +01:00
|
|
|
|
|
|
|
if ($this->parent_context) {
|
2021-03-05 06:39:19 +01:00
|
|
|
$this->parent_context->removeMutableObjectVars($methods_only);
|
2021-02-26 00:11:58 +01:00
|
|
|
}
|
2017-04-15 03:32:14 +02:00
|
|
|
}
|
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function updateChecks(Context $op_context): void
|
2016-10-18 22:14:52 +02:00
|
|
|
{
|
|
|
|
$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
|
|
|
|
2020-09-07 01:36:47 +02:00
|
|
|
public function isPhantomClass(string $class_name): bool
|
2016-10-22 19:23:18 +02:00
|
|
|
{
|
2017-03-01 17:56:36 +01:00
|
|
|
return isset($this->phantom_classes[strtolower($class_name)]);
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
|
2021-09-25 16:05:47 +02:00
|
|
|
public function hasVariable(string $var_name): bool
|
2017-02-01 05:24:33 +01:00
|
|
|
{
|
2019-02-26 07:03:33 +01:00
|
|
|
if (!$var_name) {
|
2017-11-28 06:46:41 +01:00
|
|
|
return false;
|
|
|
|
}
|
2017-02-01 05:24:33 +01:00
|
|
|
|
2017-11-28 06:46:41 +01:00
|
|
|
$stripped_var = preg_replace('/(->|\[).*$/', '', $var_name);
|
2017-02-01 05:24:33 +01:00
|
|
|
|
2019-10-04 00:13:04 +02:00
|
|
|
if ($stripped_var !== '$this' || $var_name !== $stripped_var) {
|
2017-11-28 06:46:41 +01:00
|
|
|
$this->referenced_var_ids[$var_name] = true;
|
2018-01-25 07:04:26 +01:00
|
|
|
|
2019-02-26 07:03:33 +01:00
|
|
|
if (!isset($this->vars_possibly_in_scope[$var_name])
|
|
|
|
&& !isset($this->vars_in_scope[$var_name])
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
2017-02-01 05:24:33 +01:00
|
|
|
}
|
|
|
|
|
2017-11-28 06:46:41 +01:00
|
|
|
return isset($this->vars_in_scope[$var_name]);
|
2017-02-01 05:24:33 +01:00
|
|
|
}
|
2019-01-31 18:55:48 +01:00
|
|
|
|
|
|
|
public function getScopeSummary() : string
|
|
|
|
{
|
|
|
|
$summary = [];
|
|
|
|
foreach ($this->vars_possibly_in_scope as $k => $_) {
|
|
|
|
$summary[$k] = true;
|
|
|
|
}
|
|
|
|
foreach ($this->vars_in_scope as $k => $v) {
|
|
|
|
$summary[$k] = $v->getId();
|
|
|
|
}
|
2019-07-05 22:24:00 +02:00
|
|
|
|
2019-01-31 18:55:48 +01:00
|
|
|
return json_encode($summary);
|
|
|
|
}
|
2019-03-11 14:54:41 +01:00
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function defineGlobals(): void
|
2019-03-11 14:54:41 +01:00
|
|
|
{
|
|
|
|
$globals = [
|
2019-03-14 01:15:29 +01:00
|
|
|
'$argv' => new Type\Union([
|
2019-03-11 14:54:41 +01:00
|
|
|
new Type\Atomic\TArray([Type::getInt(), Type::getString()]),
|
|
|
|
]),
|
2019-03-14 01:15:29 +01:00
|
|
|
'$argc' => Type::getInt(),
|
2019-03-11 14:54:41 +01:00
|
|
|
];
|
|
|
|
|
|
|
|
$config = Config::getInstance();
|
|
|
|
|
2019-03-14 01:15:29 +01:00
|
|
|
foreach ($config->globals as $global_id => $type_string) {
|
|
|
|
$globals[$global_id] = Type::parseString($type_string);
|
2019-03-11 14:54:41 +01:00
|
|
|
}
|
|
|
|
|
2019-03-14 01:15:29 +01:00
|
|
|
foreach ($globals as $global_id => $type) {
|
|
|
|
$this->vars_in_scope[$global_id] = $type;
|
|
|
|
$this->vars_possibly_in_scope[$global_id] = true;
|
2019-03-11 14:54:41 +01:00
|
|
|
}
|
|
|
|
}
|
2019-03-29 00:43:14 +01:00
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function mergeExceptions(Context $other_context): void
|
2019-03-29 00:43:14 +01:00
|
|
|
{
|
2019-04-03 01:42:23 +02:00
|
|
|
foreach ($other_context->possibly_thrown_exceptions as $possibly_thrown_exception => $codelocations) {
|
|
|
|
foreach ($codelocations as $hash => $codelocation) {
|
|
|
|
$this->possibly_thrown_exceptions[$possibly_thrown_exception][$hash] = $codelocation;
|
|
|
|
}
|
|
|
|
}
|
2019-03-29 00:43:14 +01:00
|
|
|
}
|
2020-09-21 21:16:19 +02:00
|
|
|
|
2020-09-04 22:26:33 +02:00
|
|
|
public function isSuppressingExceptions(StatementsAnalyzer $statements_analyzer): bool
|
2019-03-29 00:50:29 +01:00
|
|
|
{
|
|
|
|
if (!$this->collect_exceptions) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
$issue_type = $this->is_global ? 'UncaughtThrowInGlobalScope' : 'MissingThrowsDocblock';
|
|
|
|
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
|
2019-10-04 20:01:58 +02:00
|
|
|
$suppressed_issue_position = array_search($issue_type, $suppressed_issues, true);
|
|
|
|
if ($suppressed_issue_position !== false) {
|
|
|
|
if (is_int($suppressed_issue_position)) {
|
|
|
|
$file = $statements_analyzer->getFileAnalyzer()->getFilePath();
|
|
|
|
IssueBuffer::addUsedSuppressions([
|
|
|
|
$file => [$suppressed_issue_position => true],
|
|
|
|
]);
|
|
|
|
}
|
2019-03-29 00:50:29 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-03-29 00:43:14 +01:00
|
|
|
public function mergeFunctionExceptions(
|
|
|
|
FunctionLikeStorage $function_storage,
|
|
|
|
CodeLocation $codelocation
|
2020-09-12 17:24:05 +02:00
|
|
|
): void {
|
2019-04-03 01:42:23 +02:00
|
|
|
$hash = $codelocation->getHash();
|
2019-03-29 00:43:14 +01:00
|
|
|
foreach ($function_storage->throws as $possibly_thrown_exception => $_) {
|
2019-04-03 01:42:23 +02:00
|
|
|
$this->possibly_thrown_exceptions[$possibly_thrown_exception][$hash] = $codelocation;
|
2019-03-29 00:43:14 +01:00
|
|
|
}
|
|
|
|
}
|
2021-06-25 16:14:49 +02:00
|
|
|
|
|
|
|
public function insideUse(): bool
|
|
|
|
{
|
|
|
|
return $this->inside_assignment
|
|
|
|
|| $this->inside_return
|
|
|
|
|| $this->inside_call
|
|
|
|
|| $this->inside_general_use
|
|
|
|
|| $this->inside_conditional
|
|
|
|
|| $this->inside_throw
|
|
|
|
|| $this->inside_isset;
|
|
|
|
}
|
2016-06-20 06:38:13 +02:00
|
|
|
}
|