/**
* 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);
}