/** * Analyze the parameters and arguments for a call * to the given method or function * * @param CodeBase $code_base * @param Method $method * @param Node $node * * @return null */ private function analyzeCallToMethod(CodeBase $code_base, FunctionInterface $method, Node $node) { $method->addReference($this->context); // Create variables for any pass-by-reference // parameters $argument_list = $node->children['args']; foreach ($argument_list->children as $i => $argument) { if (!is_object($argument)) { continue; } $parameter = $method->getParameterForCaller($i); if (!$parameter) { continue; } // If pass-by-reference, make sure the variable exists // or create it if it doesn't. if ($parameter->isPassByReference()) { if ($argument->kind == \ast\AST_VAR) { // We don't do anything with it; just create it // if it doesn't exist $variable = (new ContextNode($this->code_base, $this->context, $argument))->getOrCreateVariable(); } elseif ($argument->kind == \ast\AST_STATIC_PROP || $argument->kind == \ast\AST_PROP) { $property_name = $argument->children['prop']; if (is_string($property_name)) { // We don't do anything with it; just create it // if it doesn't exist try { $property = (new ContextNode($this->code_base, $this->context, $argument))->getOrCreateProperty($argument->children['prop']); } catch (IssueException $exception) { Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance()); } catch (\Exception $exception) { // If we can't figure out what kind of a call // this is, don't worry about it } } else { // This is stuff like `Class->$foo`. I'm ignoring // it. } } } } // Confirm the argument types are clean ArgumentType::analyze($method, $node, $this->context, $this->code_base); // Take another pass over pass-by-reference parameters // and assign types to passed in variables foreach ($argument_list->children as $i => $argument) { if (!is_object($argument)) { continue; } $parameter = $method->getParameterForCaller($i); if (!$parameter) { continue; } if (Config::get()->dead_code_detection) { (new ArgumentVisitor($this->code_base, $this->context))($argument); } // If the parameter is pass-by-reference and we're // passing a variable in, see if we should pass // the parameter and variable types to eachother $variable = null; if ($parameter->isPassByReference()) { if ($argument->kind == \ast\AST_VAR) { $variable = (new ContextNode($this->code_base, $this->context, $argument))->getOrCreateVariable(); } elseif ($argument->kind == \ast\AST_STATIC_PROP || $argument->kind == \ast\AST_PROP) { $property_name = $argument->children['prop']; if (is_string($property_name)) { // We don't do anything with it; just create it // if it doesn't exist try { $variable = (new ContextNode($this->code_base, $this->context, $argument))->getOrCreateProperty($argument->children['prop']); } catch (IssueException $exception) { Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance()); } catch (\Exception $exception) { // If we can't figure out what kind of a call // this is, don't worry about it } } else { // This is stuff like `Class->$foo`. I'm ignoring // it. } } if ($variable) { $variable->getUnionType()->addUnionType($parameter->getVariadicElementUnionType()); } } } // If we're in quick mode, don't retest methods based on // parameter types passed in if (Config::get()->quick_mode) { return; } // We're going to hunt to see if any of the arguments // have a mismatch with the parameters. If so, we'll // re-check the method to see how the parameters impact // its return type $has_argument_parameter_mismatch = false; // Now that we've made sure the arguments are sufficient // for definitions on the method, we iterate over the // arguments again and add their types to the parameter // types so we can test the method again $argument_list = $node->children['args']; // We create a copy of the parameter list so we can switch // back to it after $original_parameter_list = $method->getParameterList(); // Create a backup of the method's scope so that we can // reset it after f*****g with it below $original_method_scope = $method->getInternalScope(); foreach ($argument_list->children as $i => $argument) { // TODO(Issue #376): Support inference on the child in **the set of vargs**, not just the first vararg // This is just testing the first vararg. // The implementer will also need to restore the original parameter list. $parameter = $original_parameter_list[$i] ?? null; if (!$parameter) { continue; } // If the parameter has no type, pass the // argument's type to it if ($parameter->getVariadicElementUnionType()->isEmpty()) { $has_argument_parameter_mismatch = true; // If this isn't an internal function or method // and it has no type, add the argument's type // to it so we can compare it to subsequent // calls if (!$parameter->isInternal()) { $argument_type = UnionType::fromNode($this->context, $this->code_base, $argument); // Clone the parameter in the original // parameter list so we can reset it // later // TODO: If there are varargs and this is beyond the end, ensure last arg is cloned. $original_parameter_list[$i] = clone $original_parameter_list[$i]; // Then set the new type on that parameter based // on the argument's type. We'll use this to // retest the method with the passed in types $parameter->getVariadicElementUnionType()->addUnionType($argument_type); if (!is_object($argument)) { continue; } // If we're passing by reference, get the variable // we're dealing with wrapped up and shoved into // the scope of the method if ($parameter->isPassByReference()) { if ($original_parameter_list[$i]->isVariadic()) { // For now, give up and work on it later. // TODO(Issue #376): It's possible to have a parameter `&...$args`. Analysing that is going to be a problem. // Is it possible to create `PassByReferenceVariableCollection extends Variable` or something similar? } elseif ($argument->kind == \ast\AST_VAR) { // Get the variable $variable = (new ContextNode($this->code_base, $this->context, $argument))->getOrCreateVariable(); $pass_by_reference_variable = new PassByReferenceVariable($parameter, $variable); $parameter_list = $method->getParameterList(); $parameter_list[$i] = $pass_by_reference_variable; $method->setParameterList($parameter_list); // Add it to the scope of the function wrapped // in a way that makes it addressable as the // parameter its mimicking $method->getInternalScope()->addVariable($pass_by_reference_variable); } else { if ($argument->kind == \ast\AST_STATIC_PROP) { // Get the variable $property = (new ContextNode($this->code_base, $this->context, $argument))->getOrCreateProperty($argument->children['prop'] ?? ''); $pass_by_reference_variable = new PassByReferenceVariable($parameter, $property); $parameter_list = $method->getParameterList(); $parameter_list[$i] = $pass_by_reference_variable; $method->setParameterList($parameter_list); // Add it to the scope of the function wrapped // in a way that makes it addressable as the // parameter its mimicking $method->getInternalScope()->addVariable($pass_by_reference_variable); } } } else { // Overwrite the method's variable representation // of the parameter with the parameter with the // new type $method->getInternalScope()->addVariable($parameter); } } } } // Now that we know something about the parameters used // to call the method, we can reanalyze the method with // the types of the parameter, making sure we don't get // into an infinite loop of checking calls to the current // method in scope if ($has_argument_parameter_mismatch && !$method->isInternal() && (!$this->context->isInFunctionLikeScope() || $method->getFQSEN() !== $this->context->getFunctionLikeFQSEN())) { $method->analyze($method->getContext(), $code_base); } // Reset to the original parameter list after having // tested the parameters with the types passed in $method->setParameterList($original_parameter_list); // Reset the scope to its original version before we // put new parameters in it $method->setInternalScope($original_method_scope); }