*/ private $all_vars = []; /** * @var array */ private $var_branch_points = []; /** * Possibly undefined variables should be initialised if we're altering code * * @var array|null */ private $vars_to_initialize; /** * @var array */ private $function_checkers = []; /** * @var array */ private $unused_var_locations = []; /** * @param StatementsSource $source */ public function __construct(StatementsSource $source) { $this->source = $source; $this->file_checker = $source->getFileChecker(); } /** * Checks an array of statements for validity * * @param array $stmts * @param Context $context * @param Context|null $global_context * @param bool $root_scope * * @return null|false */ public function analyze( array $stmts, Context $context, LoopScope $loop_scope = null, Context $global_context = null, $root_scope = false ) { $has_returned = false; // hoist functions to the top foreach ($stmts as $stmt) { if ($stmt instanceof PhpParser\Node\Stmt\Function_) { $function_checker = new FunctionChecker($stmt, $this->source); $this->function_checkers[strtolower($stmt->name)] = $function_checker; } } $project_checker = $this->getFileChecker()->project_checker; $codebase = $project_checker->codebase; $original_context = null; if ($loop_scope) { $original_context = clone $context; } $plugin_classes = $codebase->config->after_statement_checks; foreach ($stmts as $stmt) { if ($has_returned && !($stmt instanceof PhpParser\Node\Stmt\Nop) && !($stmt instanceof PhpParser\Node\Stmt\InlineHTML) ) { if ($context->collect_references) { if (IssueBuffer::accepts( new UnevaluatedCode( 'Expressions after return/throw/continue', new CodeLocation($this->source, $stmt) ), $this->source->getSuppressedIssues() )) { return false; } } break; } /* if (isset($context->vars_in_scope['$tag']) && !$stmt instanceof PhpParser\Node\Stmt\Nop) { var_dump($stmt->getLine() . ' ' . $context->vars_in_scope['$tag']); } */ $new_issues = null; if ($docblock = $stmt->getDocComment()) { $comments = CommentChecker::parseDocComment((string)$docblock); if (isset($comments['specials']['psalm-suppress'])) { $suppressed = array_filter( array_map( /** * @param string $line * * @return string */ function ($line) { return explode(' ', trim($line))[0]; }, $comments['specials']['psalm-suppress'] ) ); if ($suppressed) { $new_issues = array_diff($suppressed, $this->source->getSuppressedIssues()); /** @psalm-suppress MixedTypeCoercion */ $this->addSuppressedIssues($new_issues); } } } if ($stmt instanceof PhpParser\Node\Stmt\If_) { if (IfChecker::analyze($this, $stmt, $context, $loop_scope) === false) { return false; } } elseif ($stmt instanceof PhpParser\Node\Stmt\TryCatch) { if (TryChecker::analyze($this, $stmt, $context, $loop_scope) === false) { return false; } } elseif ($stmt instanceof PhpParser\Node\Stmt\For_) { if (ForChecker::analyze($this, $stmt, $context) === false) { return false; } } elseif ($stmt instanceof PhpParser\Node\Stmt\Foreach_) { if (ForeachChecker::analyze($this, $stmt, $context) === false) { return false; } } elseif ($stmt instanceof PhpParser\Node\Stmt\While_) { if (WhileChecker::analyze($this, $stmt, $context) === false) { return false; } } elseif ($stmt instanceof PhpParser\Node\Stmt\Do_) { DoChecker::analyze($this, $stmt, $context); } elseif ($stmt instanceof PhpParser\Node\Stmt\Const_) { $this->analyzeConstAssignment($stmt, $context); } elseif ($stmt instanceof PhpParser\Node\Stmt\Unset_) { $context->inside_unset = true; foreach ($stmt->vars as $var) { ExpressionChecker::analyze($this, $var, $context); $var_id = ExpressionChecker::getArrayVarId( $var, $this->getFQCLN(), $this ); if ($var_id) { $context->remove($var_id); } } $context->inside_unset = false; } elseif ($stmt instanceof PhpParser\Node\Stmt\Return_) { $has_returned = true; ReturnChecker::analyze($this, $project_checker, $stmt, $context); } elseif ($stmt instanceof PhpParser\Node\Stmt\Throw_) { $has_returned = true; ThrowChecker::analyze($this, $stmt, $context); } elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) { SwitchChecker::analyze($this, $stmt, $context, $loop_scope); } elseif ($stmt instanceof PhpParser\Node\Stmt\Break_) { if ($loop_scope && $original_context) { $loop_scope->final_actions[] = ScopeChecker::ACTION_BREAK; $redefined_vars = $context->getRedefinedVars($loop_scope->loop_parent_context->vars_in_scope); if ($loop_scope->possibly_redefined_loop_parent_vars === null) { $loop_scope->possibly_redefined_loop_parent_vars = $redefined_vars; } else { foreach ($redefined_vars as $var => $type) { if ($type->isMixed()) { $loop_scope->possibly_redefined_loop_parent_vars[$var] = $type; } elseif (isset($loop_scope->possibly_redefined_loop_parent_vars[$var])) { $loop_scope->possibly_redefined_loop_parent_vars[$var] = Type::combineUnionTypes( $type, $loop_scope->possibly_redefined_loop_parent_vars[$var] ); } else { $loop_scope->possibly_redefined_loop_parent_vars[$var] = $type; } } } } $has_returned = true; } elseif ($stmt instanceof PhpParser\Node\Stmt\Continue_) { if ($loop_scope === null) { if (!$context->inside_case) { if (IssueBuffer::accepts( new ContinueOutsideLoop( 'Continue call outside loop context', new CodeLocation($this->source, $stmt) ), $this->source->getSuppressedIssues() )) { return false; } } } elseif ($original_context) { $loop_scope->final_actions[] = ScopeChecker::ACTION_CONTINUE; $redefined_vars = $context->getRedefinedVars($original_context->vars_in_scope); if ($loop_scope->redefined_loop_vars === null) { $loop_scope->redefined_loop_vars = $redefined_vars; } else { foreach ($loop_scope->redefined_loop_vars as $redefined_var => $type) { if (!isset($redefined_vars[$redefined_var])) { unset($loop_scope->redefined_loop_vars[$redefined_var]); } else { $loop_scope->redefined_loop_vars[$redefined_var] = Type::combineUnionTypes( $redefined_vars[$redefined_var], $type ); } } } foreach ($redefined_vars as $var => $type) { if ($type->isMixed()) { $loop_scope->possibly_redefined_loop_vars[$var] = $type; } elseif (isset($loop_scope->possibly_redefined_loop_vars[$var])) { $loop_scope->possibly_redefined_loop_vars[$var] = Type::combineUnionTypes( $type, $loop_scope->possibly_redefined_loop_vars[$var] ); } else { $loop_scope->possibly_redefined_loop_vars[$var] = $type; } } } $has_returned = true; } elseif ($stmt instanceof PhpParser\Node\Stmt\Static_) { $this->analyzeStatic($stmt, $context); } elseif ($stmt instanceof PhpParser\Node\Stmt\Echo_) { foreach ($stmt->exprs as $i => $expr) { ExpressionChecker::analyze($this, $expr, $context); if (isset($expr->inferredType)) { if (CallChecker::checkFunctionArgumentType( $this, $expr->inferredType, Type::getString(), 'echo', (int)$i, new CodeLocation($this->getSource(), $expr), $expr, $context ) === false) { return false; } } } } elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) { foreach ($stmt->stmts as $function_stmt) { if ($function_stmt instanceof PhpParser\Node\Stmt\Global_) { foreach ($function_stmt->vars as $var) { if ($var instanceof PhpParser\Node\Expr\Variable) { if (is_string($var->name)) { $var_id = '$' . $var->name; // registers variable in global context $context->hasVariable($var_id, $this); } } } } elseif (!$function_stmt instanceof PhpParser\Node\Stmt\Nop) { break; } } if (!$project_checker->codebase->register_global_functions) { $function_id = strtolower($stmt->name); $function_context = new Context($context->self); $function_context->collect_references = $project_checker->codebase->collect_references; $this->function_checkers[$function_id]->analyze($function_context, $context); $config = Config::getInstance(); if ($config->reportIssueInFile('InvalidReturnType', $this->getFilePath())) { /** @var string */ $method_id = $this->function_checkers[$function_id]->getMethodId(); $function_storage = $codebase->functions->getStorage( $this, $method_id ); $return_type = $function_storage->return_type; $return_type_location = $function_storage->return_type_location; $this->function_checkers[$function_id]->verifyReturnType( $return_type, $this->getFQCLN(), $return_type_location ); } } } elseif ($stmt instanceof PhpParser\Node\Expr) { if (ExpressionChecker::analyze($this, $stmt, $context) === false) { return false; } } elseif ($stmt instanceof PhpParser\Node\Stmt\InlineHTML) { // do nothing } elseif ($stmt instanceof PhpParser\Node\Stmt\Global_) { if (!$context->collect_initializations && !$global_context) { if (IssueBuffer::accepts( new InvalidGlobal( 'Cannot use global scope here', new CodeLocation($this->source, $stmt) ), $this->source->getSuppressedIssues() )) { // fall through } } foreach ($stmt->vars as $var) { if ($var instanceof PhpParser\Node\Expr\Variable) { if (is_string($var->name)) { $var_id = '$' . $var->name; $context->vars_in_scope[$var_id] = $global_context && $global_context->hasVariable($var_id, $this) ? clone $global_context->vars_in_scope[$var_id] : Type::getMixed(); $context->vars_possibly_in_scope[$var_id] = true; } else { ExpressionChecker::analyze($this, $var, $context); } } } } elseif ($stmt instanceof PhpParser\Node\Stmt\Property) { foreach ($stmt->props as $prop) { if ($prop->default) { ExpressionChecker::analyze($this, $prop->default, $context); if (isset($prop->default->inferredType)) { if (!$stmt->isStatic()) { if (PropertyAssignmentChecker::analyzeInstance( $this, $prop, $prop->name, $prop->default, $prop->default->inferredType, $context ) === false) { // fall through } } } } } } elseif ($stmt instanceof PhpParser\Node\Stmt\ClassConst) { $const_visibility = \ReflectionProperty::IS_PUBLIC; if ($stmt->isProtected()) { $const_visibility = \ReflectionProperty::IS_PROTECTED; } if ($stmt->isPrivate()) { $const_visibility = \ReflectionProperty::IS_PRIVATE; } foreach ($stmt->consts as $const) { ExpressionChecker::analyze($this, $const->value, $context); if (isset($const->value->inferredType) && !$const->value->inferredType->isMixed()) { $codebase->classlikes->setConstantType( (string)$this->getFQCLN(), $const->name, $const->value->inferredType, $const_visibility ); } } } elseif ($stmt instanceof PhpParser\Node\Stmt\Class_) { try { $class_checker = (new ClassChecker($stmt, $this->source, $stmt->name)); $class_checker->analyze(null, $global_context); } catch (\InvalidArgumentException $e) { // disregard this exception, we'll likely see it elsewhere in the form // of an issue } } elseif ($stmt instanceof PhpParser\Node\Stmt\Nop) { if ((string)$stmt->getDocComment()) { $var_comments = []; try { $var_comments = CommentChecker::getTypeFromComment( (string)$stmt->getDocComment(), $this->getSource(), $this->getSource()->getAliases() ); } catch (DocblockParseException $e) { if (IssueBuffer::accepts( new InvalidDocblock( (string)$e->getMessage(), new CodeLocation($this->getSource(), $stmt, null, true) ) )) { // fall through } } foreach ($var_comments as $var_comment) { if (!$var_comment->var_id) { continue; } $comment_type = ExpressionChecker::fleshOutType( $project_checker, $var_comment->type, $context->self ); $context->vars_in_scope[$var_comment->var_id] = $comment_type; } } } elseif ($stmt instanceof PhpParser\Node\Stmt\Goto_) { // do nothing } elseif ($stmt instanceof PhpParser\Node\Stmt\Label) { // do nothing } elseif ($stmt instanceof PhpParser\Node\Stmt\Declare_) { // do nothing } else { if (IssueBuffer::accepts( new UnrecognizedStatement( 'Psalm does not understand ' . get_class($stmt), new CodeLocation($this->source, $stmt) ), $this->getSuppressedIssues() )) { return false; } } if ($loop_scope && $loop_scope->final_actions && !in_array(ScopeChecker::ACTION_NONE, $loop_scope->final_actions, true) ) { //$has_returned = true; } if ($plugin_classes) { $file_manipulations = []; $code_location = new CodeLocation($this->source, $stmt); foreach ($plugin_classes as $plugin_fq_class_name) { if ($plugin_fq_class_name::afterStatementCheck( $this, $stmt, $context, $code_location, $this->getSuppressedIssues(), $file_manipulations ) === false) { return false; } } if ($file_manipulations) { /** @psalm-suppress MixedTypeCoercion */ FileManipulationBuffer::add($this->getFilePath(), $file_manipulations); } } if ($new_issues) { /** @psalm-suppress MixedTypeCoercion */ $this->removeSuppressedIssues($new_issues); } } if ($root_scope && $context->collect_references && !$context->collect_initializations && $project_checker->codebase->find_unused_code && $context->check_variables ) { $this->checkUnreferencedVars(); } if ($project_checker->alter_code && $root_scope && $this->vars_to_initialize) { $file_contents = $project_checker->codebase->getFileContents($this->getFilePath()); foreach ($this->vars_to_initialize as $var_id => $branch_point) { $newline_pos = (int)strrpos($file_contents, "\n", $branch_point - strlen($file_contents)) + 1; $indentation = substr($file_contents, $newline_pos, $branch_point - $newline_pos); FileManipulationBuffer::add($this->getFilePath(), [ new FileManipulation($branch_point, $branch_point, $var_id . ' = null;' . PHP_EOL . $indentation), ]); } } return null; } /** * @return void */ public function checkUnreferencedVars() { $source = $this->getSource(); $function_storage = $source instanceof FunctionLikeChecker ? $source->getFunctionLikeStorage($this) : null; foreach ($this->unused_var_locations as list($var_id, $original_location)) { if ($var_id === '$_') { continue; } if (!$function_storage || !array_key_exists(substr($var_id, 1), $function_storage->param_types)) { if (IssueBuffer::accepts( new UnusedVariable( 'Variable ' . $var_id . ' is never referenced', $original_location ), $this->getSuppressedIssues() )) { // fall through } } } } /** * @param PhpParser\Node\Stmt\Static_ $stmt * @param Context $context * * @return false|null */ private function analyzeStatic(PhpParser\Node\Stmt\Static_ $stmt, Context $context) { foreach ($stmt->vars as $var) { if ($var->default) { if (ExpressionChecker::analyze($this, $var->default, $context) === false) { return false; } } if ($context->check_variables) { $var_id = '$' . $var->name; $context->vars_in_scope[$var_id] = Type::getMixed(); $context->vars_possibly_in_scope[$var_id] = true; $context->assigned_var_ids[$var_id] = true; $location = new CodeLocation($this, $stmt); if ($context->collect_references) { $context->unreferenced_vars[$var_id] = $location; } $this->registerVariable( $var_id, $location, $context->branch_point ); } } return null; } /** * @param PhpParser\Node\Expr $stmt * @param array $existing_class_constants * * @return Type\Union|null */ public static function getSimpleType( PhpParser\Node\Expr $stmt, StatementsSource $statements_source = null, array $existing_class_constants = [] ) { if ($stmt instanceof PhpParser\Node\Expr\BinaryOp) { if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat) { return Type::getString(); } if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd || $stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr || $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd || $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal || $stmt instanceof PhpParser\Node\Expr\BinaryOp\NotEqual || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical || $stmt instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Greater || $stmt instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Smaller || $stmt instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual ) { return Type::getBool(); } if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Coalesce) { return null; } if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Spaceship) { return Type::getInt(); } $stmt->left->inferredType = self::getSimpleType( $stmt->left, $statements_source, $existing_class_constants ); $stmt->right->inferredType = self::getSimpleType( $stmt->right, $statements_source, $existing_class_constants ); if (!$stmt->left->inferredType || !$stmt->right->inferredType) { return null; } if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Plus || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Minus || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Mod || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Mul || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Pow ) { BinaryOpChecker::analyzeNonDivArithmenticOp( $statements_source, $stmt->left, $stmt->right, $stmt, $result_type ); if ($result_type) { return $result_type; } return null; } if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Div && ($stmt->left->inferredType->hasInt() || $stmt->left->inferredType->hasFloat()) && ($stmt->right->inferredType->hasInt() || $stmt->right->inferredType->hasFloat()) ) { return Type::combineUnionTypes(Type::getFloat(), Type::getInt()); } } if ($stmt instanceof PhpParser\Node\Expr\ConstFetch) { if (strtolower($stmt->name->parts[0]) === 'false') { return Type::getFalse(); } elseif (strtolower($stmt->name->parts[0]) === 'true') { return Type::getBool(); } elseif (strtolower($stmt->name->parts[0]) === 'null') { return Type::getNull(); } return null; } if ($stmt instanceof PhpParser\Node\Expr\ClassConstFetch) { if ($stmt->class instanceof PhpParser\Node\Name && $stmt->class->parts !== ['static'] && is_string($stmt->name) && isset($existing_class_constants[$stmt->name]) ) { return $existing_class_constants[$stmt->name]; } if (strtolower($stmt->name) === 'class') { return Type::getClassString(); } return null; } if ($stmt instanceof PhpParser\Node\Scalar\String_) { return Type::getString(); } if ($stmt instanceof PhpParser\Node\Scalar\LNumber) { return Type::getInt(); } if ($stmt instanceof PhpParser\Node\Scalar\DNumber) { return Type::getFloat(); } if ($stmt instanceof PhpParser\Node\Expr\Array_) { if (count($stmt->items) === 0) { return Type::getEmptyArray(); } $item_key_type = null; $item_value_type = null; $property_types = []; $can_create_objectlike = true; foreach ($stmt->items as $int_offset => $item) { if ($item === null) { continue; } if ($item->key) { $single_item_key_type = self::getSimpleType( $item->key, $statements_source, $existing_class_constants ); if ($single_item_key_type) { if ($item_key_type) { $item_key_type = Type::combineUnionTypes($single_item_key_type, $item_key_type); } else { $item_key_type = $single_item_key_type; } } } else { $item_key_type = Type::getInt(); } if ($item_value_type && $item_value_type->isMixed() && !$can_create_objectlike) { continue; } $single_item_value_type = self::getSimpleType( $item->value, $statements_source, $existing_class_constants ); if ($single_item_value_type) { if ($item->key instanceof PhpParser\Node\Scalar\String_ || $item->key instanceof PhpParser\Node\Scalar\LNumber || !$item->key ) { $property_types[$item->key ? $item->key->value : $int_offset] = $single_item_value_type; } else { $can_create_objectlike = false; } if ($item_value_type) { $item_value_type = Type::combineUnionTypes($single_item_value_type, $item_value_type); } else { $item_value_type = $single_item_value_type; } } else { $item_value_type = Type::getMixed(); if ($item->key instanceof PhpParser\Node\Scalar\String_ || $item->key instanceof PhpParser\Node\Scalar\LNumber || !$item->key ) { $property_types[$item->key ? $item->key->value : $int_offset] = $item_value_type; } else { $can_create_objectlike = false; } } } // if this array looks like an object-like array, let's return that instead if ($item_value_type && $item_key_type && ($item_key_type->hasString() || $item_key_type->hasInt()) && $can_create_objectlike ) { return new Type\Union([new Type\Atomic\ObjectLike($property_types)]); } return new Type\Union([ new Type\Atomic\TArray([ $item_key_type ?: Type::getMixed(), $item_value_type ?: Type::getMixed(), ]), ]); } if ($stmt instanceof PhpParser\Node\Expr\Cast\Int_) { return Type::getInt(); } if ($stmt instanceof PhpParser\Node\Expr\Cast\Double) { return Type::getFloat(); } if ($stmt instanceof PhpParser\Node\Expr\Cast\Bool_) { return Type::getBool(); } if ($stmt instanceof PhpParser\Node\Expr\Cast\String_) { return Type::getString(); } if ($stmt instanceof PhpParser\Node\Expr\Cast\Object_) { return Type::getObject(); } if ($stmt instanceof PhpParser\Node\Expr\Cast\Array_) { return Type::getArray(); } if ($stmt instanceof PhpParser\Node\Expr\UnaryMinus || $stmt instanceof PhpParser\Node\Expr\UnaryPlus) { return self::getSimpleType($stmt->expr, $statements_source, $existing_class_constants); } return null; } /** * @param PhpParser\Node\Stmt\Const_ $stmt * @param Context $context * * @return void */ private function analyzeConstAssignment(PhpParser\Node\Stmt\Const_ $stmt, Context $context) { foreach ($stmt->consts as $const) { ExpressionChecker::analyze($this, $const->value, $context); $this->setConstType( $const->name, isset($const->value->inferredType) ? $const->value->inferredType : Type::getMixed(), $context ); } } /** * @param string $const_name * @param bool $is_fully_qualified * @param Context $context * * @return Type\Union|null */ public function getConstType( StatementsChecker $statements_checker, $const_name, $is_fully_qualified, Context $context ) { $fq_const_name = null; $aliased_constants = $this->getAliases()->constants; if (isset($aliased_constants[$const_name])) { $fq_const_name = $aliased_constants[$const_name]; } elseif ($is_fully_qualified) { $fq_const_name = $const_name; } elseif (strpos($const_name, '\\')) { $fq_const_name = Type::getFQCLNFromString($const_name, $this->getAliases()); } if ($fq_const_name) { $const_name_parts = explode('\\', $fq_const_name); /** @var string */ $const_name = array_pop($const_name_parts); $namespace_name = implode('\\', $const_name_parts); $namespace_constants = NamespaceChecker::getConstantsForNamespace( $namespace_name, \ReflectionProperty::IS_PUBLIC ); if (isset($namespace_constants[$const_name])) { return $namespace_constants[$const_name]; } } if ($context->hasVariable($const_name, $statements_checker)) { return $context->vars_in_scope[$const_name]; } $file_path = $statements_checker->getFilePath(); $project_checker = $statements_checker->getFileChecker()->project_checker; $file_storage_provider = $project_checker->file_storage_provider; $file_storage = $file_storage_provider->get($file_path); if (isset($file_storage->declaring_constants[$const_name])) { $constant_file_path = $file_storage->declaring_constants[$const_name]; return $file_storage_provider->get($constant_file_path)->constants[$const_name]; } $predefined_constants = Config::getInstance()->getPredefinedConstants(); if (isset($predefined_constants[$fq_const_name ?: $const_name])) { return ClassLikeChecker::getTypeFromValue($predefined_constants[$fq_const_name ?: $const_name]); } $stubbed_const_type = $project_checker->codebase->getStubbedConstantType( $fq_const_name ?: $const_name ); if ($stubbed_const_type) { return $stubbed_const_type; } return null; } /** * @param string $const_name * @param Type\Union $const_type * @param Context $context * * @return void */ public function setConstType($const_name, Type\Union $const_type, Context $context) { $context->vars_in_scope[$const_name] = $const_type; $context->constants[$const_name] = $const_type; if ($this->source instanceof NamespaceChecker) { $this->source->setConstType($const_name, $const_type); } } /** * @param string $var_name * * @return bool */ public function hasVariable($var_name) { return isset($this->all_vars[$var_name]); } /** * @param string $var_id * @param CodeLocation $location * @param int|null $branch_point * * @return void */ public function registerVariable($var_id, CodeLocation $location, $branch_point) { $this->all_vars[$var_id] = $location; if ($branch_point) { $this->var_branch_points[$var_id] = $branch_point; } $this->registerVariableAssignment($var_id, $location); } /** * @param string $var_id * @param CodeLocation $location * * @return void */ public function registerVariableAssignment($var_id, CodeLocation $location) { $this->unused_var_locations[spl_object_hash($location)] = [$var_id, $location]; } /** * @return void */ public function registerVariableUse(CodeLocation $location) { unset($this->unused_var_locations[spl_object_hash($location)]); } /** * @return array */ public function getUnusedVarLocations() { return $this->unused_var_locations; } /** * The first appearance of the variable in this set of statements being evaluated * * @param string $var_id * * @return CodeLocation|null */ public function getFirstAppearance($var_id) { return isset($this->all_vars[$var_id]) ? $this->all_vars[$var_id] : null; } /** * @param string $var_id * * @return int|null */ public function getBranchPoint($var_id) { return isset($this->var_branch_points[$var_id]) ? $this->var_branch_points[$var_id] : null; } /** * @param string $var_id * @param int $branch_point * * @return void */ public function addVariableInitialization($var_id, $branch_point) { $this->vars_to_initialize[$var_id] = $branch_point; } /** * @return FileChecker */ public function getFileChecker() { return $this->file_checker; } /** * @return array */ public function getFunctionCheckers() { return $this->function_checkers; } }