public function __toString() : string { $string = ''; if (!$this->type->isEmpty()) { $string .= "{$this->type} "; } $string .= $this->name; return $string; }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitProp(Node $node) : Context { $property_name = $node->children['prop']; // Things like $foo->$bar if (!is_string($property_name)) { return $this->context; } assert(is_string($property_name), "Property must be string in context {$this->context}"); try { $class_list = (new ContextNode($this->code_base, $this->context, $node->children['expr']))->getClassList(); } catch (CodeBaseException $exception) { // This really shouldn't happen since the code // parsed cleanly. This should fatal. // throw $exception; return $this->context; } catch (\Exception $exception) { // If we can't figure out what kind of a class // this is, don't worry about it return $this->context; } foreach ($class_list as $clazz) { // Check to see if this class has the property or // a setter if (!$clazz->hasPropertyWithName($this->code_base, $property_name)) { if (!$clazz->hasMethodWithName($this->code_base, '__set')) { continue; } } try { $property = $clazz->getPropertyByNameInContext($this->code_base, $property_name, $this->context); } catch (IssueException $exception) { $exception->getIssueInstance()(); return $this->context; } if (!$this->right_type->canCastToExpandedUnionType($property->getUnionType(), $this->code_base)) { Issue::emit(Issue::TypeMismatchProperty, $this->context->getFile(), $node->lineno ?? 0, (string) $this->right_type, "{$clazz->getFQSEN()}::{$property->getName()}", (string) $property->getUnionType()); return $this->context; } // After having checked it, add this type to it $property->getUnionType()->addUnionType($this->right_type); return $this->context; } if (Config::get()->allow_missing_properties) { try { // Create the property (new ContextNode($this->code_base, $this->context, $node))->getOrCreateProperty($property_name); } catch (\Exception $exception) { // swallow it } } else { if (!empty($class_list)) { Issue::emit(Issue::UndeclaredProperty, $this->context->getFile(), $node->lineno ?? 0, $property_name); } else { // If we hit this part, we couldn't figure out // the class, so we ignore the issue } } return $this->context; }
/** * @param Node $node * An AST_VAR node * * @param Context $context * The context in which the variable is found * * @param CodeBase $code_base * * @return Variable * A variable begotten from a node */ public static function fromNodeInContext(Node $node, Context $context, CodeBase $code_base, bool $should_check_type = true) : Variable { $variable_name = AST::variableName($node); // Get the type of the assignment $union_type = $should_check_type ? UnionType::fromNode($context, $code_base, $node) : new UnionType(); $variable = new Variable($context->withLineNumberStart($node->lineno ?? 0)->withLineNumberEnd($node->endLineno ?? 0), $variable_name, $union_type, $node->flags); return $variable; }
/** * @param FunctionInterface $function * Get a list of methods hydrated with type information * for the given partial method * * @param CodeBase $code_base * The global code base holding all state * * @return Method[] * A list of typed methods based on the given method */ private static function functionListFromFunction(FunctionInterface $function, CodeBase $code_base) : array { // See if we have any type information for this // internal function $map_list = UnionType::internalFunctionSignatureMapForFQSEN($function->getFQSEN()); if (!$map_list) { return [$function]; } $alternate_id = 0; return array_map(function ($map) use($function, &$alternate_id) : FunctionInterface { $alternate_function = clone $function; $alternate_function->setFQSEN($alternate_function->getFQSEN()->withAlternateId($alternate_id++)); // Set the return type if one is defined if (!empty($map['return_type'])) { $alternate_function->setUnionType($map['return_type']); } // Load properties if defined foreach ($map['property_name_type_map'] ?? [] as $parameter_name => $parameter_type) { $flags = 0; $is_optional = false; // Check to see if its a pass-by-reference parameter if (strpos($parameter_name, '&') === 0) { $flags |= \ast\flags\PARAM_REF; $parameter_name = substr($parameter_name, 1); } // Check to see if its variadic if (strpos($parameter_name, '...') !== false) { $flags |= \ast\flags\PARAM_VARIADIC; $parameter_name = str_replace('...', '', $parameter_name); } // Check to see if its an optional parameter if (strpos($parameter_name, '=') !== false) { $is_optional = true; $parameter_name = str_replace('=', '', $parameter_name); } $parameter = new Parameter($function->getContext(), $parameter_name, $parameter_type, $flags); if ($is_optional) { $parameter->setDefaultValueType(NullType::instance()->asUnionType()); } // Add the parameter $alternate_function->appendParameter($parameter); } $alternate_function->setNumberOfRequiredParameters(array_reduce($alternate_function->getParameterList(), function (int $carry, Parameter $parameter) : int { return $carry + ($parameter->isOptional() ? 0 : 1); }, 0)); $alternate_function->setNumberOfOptionalParameters(count($alternate_function->getParameterList()) - $alternate_function->getNumberOfRequiredParameters()); if ($alternate_function instanceof Method) { if ($alternate_function->getIsMagicCall() || $alternate_function->getIsMagicCallStatic()) { $alternate_function->setNumberOfOptionalParameters(999); $alternate_function->setNumberOfRequiredParameters(0); } } return $alternate_function; }, $map_list); }
/** * @param array * A map from column name to value * * @return Model * An instance of the model derived from row data */ public static function fromRow(array $row) : Clazz { $parent_fqsen = $row['parent_class_fqsen'] ? FullyQualifiedClassName::fromFullyQualifiedString($row['parent_class_fqsen']) : null; $interface_fqsen_list = array_map(function (string $fqsen_string) { return FullyQualifiedClassName::fromFullyQualifiedString($fqsen_string); }, array_filter(explode('|', $row['interface_fqsen_list']))); $trait_fqsen_list = array_map(function (string $fqsen_string) { return FullyQualifiedClassName::fromFullyQualifiedString($fqsen_string); }, array_filter(explode('|', $row['trait_fqsen_list']))); $clazz = new ClazzElement(unserialize(base64_decode($row['context'])), $row['name'], UnionType::fromFullyQualifiedString($row['type']), (int) $row['flags'], $parent_fqsen, $interface_fqsen_list, $trait_fqsen_list); return new Clazz($clazz); }
/** * Add any functions from the FunctionSignatureMap that aren't * defined in this version of PHP to the code base * * @return void */ private function addUndefinedFunctionSignatures() { $function_signature_map = UnionType::internalFunctionSignatureMap(); foreach ($function_signature_map as $function_name => $signature) { $fqsen = FullyQualifiedFunctionName::make('\\', $function_name); // If we already loaded the function, skip it if ($this->hasMethod($fqsen)) { continue; } // Add each method returned for the signature foreach (Method::methodListFromSignature($this, $fqsen, $signature) as $method) { $this->addMethod($method); } } }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitProp(Node $node) : Context { $property_name = $node->children['prop']; // Things like $foo->$bar if (!is_string($property_name)) { return $this->context; } assert(is_string($property_name), "Property must be string in context {$this->context}"); try { $clazz = (new ContextNode($this->code_base, $this->context, $node))->getClass(); } catch (CodeBaseException $exception) { Log::err(Log::EFATAL, $exception->getMessage(), $this->context->getFile(), $node->lineno); } catch (\Exception $exception) { // If we can't figure out what kind of a class // this is, don't worry about it return $this->context; } if (!$clazz->hasPropertyWithName($this->code_base, $property_name)) { // Check to see if the class has a __set method if (!$clazz->hasMethodWithName($this->code_base, '__set')) { if (Config::get()->allow_missing_properties) { try { // Create the property (new ContextNode($this->code_base, $this->context, $node))->getOrCreateProperty($property_name); } catch (\Exception $exception) { // swallow it } } else { Log::err(Log::EAVAIL, "Missing property with name '{$property_name}'", $this->context->getFile(), $node->lineno); } } return $this->context; } try { $property = $clazz->getPropertyByNameInContext($this->code_base, $property_name, $this->context); } catch (AccessException $exception) { Log::err(Log::EACCESS, $exception->getMessage(), $this->context->getFile(), $node->lineno); return $this->context; } if (!$this->right_type->canCastToExpandedUnionType($property->getUnionType(), $this->code_base)) { Log::err(Log::ETYPE, "assigning {$this->right_type} to property but {$clazz->getFQSEN()}::{$property->getName()} is {$property->getUnionType()}", $this->context->getFile(), $node->lineno); return $this->context; } // After having checked it, add this type to it $property->getUnionType()->addUnionType($this->right_type); return $this->context; }
/** * @param CodeBase $code_base * The code base within which we're operating * * @param $context $context * The context of the parser at the node for which we'd * like to determine a type * * @param Node|mixed $node * The node for which we'd like to determine its type * * @return UnionType * The UnionType associated with the given node * in the given Context within the given CodeBase * * @return UnionType * The union type for a node of type \ast\AST_CLASS */ public static function unionTypeFromClassNode(CodeBase $code_base, Context $context, $node) : UnionType { // For simple nodes or very complicated nodes, // recurse if (!$node instanceof \ast\Node || $node->kind != \ast\AST_NAME) { return self::unionTypeFromNode($code_base, $context, $node); } $class_name = $node->children['name']; // Check to see if the name is fully qualified if (!($node->flags & \ast\flags\NAME_NOT_FQ)) { if (0 !== strpos($class_name, '\\')) { $class_name = '\\' . $class_name; } return UnionType::fromFullyQualifiedString($class_name); } if ('parent' === $class_name) { $class = $context->getClassInScope($code_base); $parent_class_fqsen = $class->getParentClassFQSEN(); $parent_class = $code_base->getClassByFQSEN($parent_class_fqsen); return $parent_class->getUnionType(); } return UnionType::fromStringInContext($class_name, $context); }
/** * @return string * A string representation of the union type begotten from * the first statement in the statement list in the given * code. */ private function typeStringFromCode(string $code) : string { return UnionType::fromNode($this->context, $this->code_base, \ast\parse_code($code, Config::get()->ast_version)->children[0])->asExpandedTypes($this->code_base)->__toString(); }
/** * Force the future to figure out the type of the * given object or throw an IssueException if it * is unable to do so * * @return UnionType * The type of the future * * @throws IssueException * An exception is thrown if we are unable to determine * the type at the time this method is called */ public function get() : UnionType { return UnionType::fromNode($this->context, $this->code_base, $this->node, false); }
/** * @param string $scope * The scope of the method or function * * @param string $name * The name of the method (with an optional alternate id) * * @return bool */ private function hasMethodWithScopeAndName(string $scope, string $name) { if (!empty($this->method_map[$scope][$name])) { return true; } // For elements in the root namespace, check to see if // there's a static method signature for something that // hasn't been loaded into memory yet and create a // method out of it as its requested if ('\\' == $scope) { $function_signature_map = UnionType::internalFunctionSignatureMap(); $fqsen = FullyQualifiedFunctionName::make($scope, $name); if (!empty($function_signature_map[$name])) { $signature = $function_signature_map[$name]; // Add each method returned for the signature foreach (FunctionFactory::functionListFromSignature($this, $fqsen, $signature) as $method) { $this->addMethod($method); } return true; } } if (Database::isEnabled()) { // Otherwise, check the database try { MethodModel::read(Database::get(), $scope . '|' . $name); return true; } catch (NotFoundException $exception) { return false; } } else { return false; } }
/** * @param Context $context * The context in which the node appears * * @param CodeBase $code_base * * @param Node $node * An AST node representing a function * * @return Func * A Func representing the AST node in the * given context */ public static function fromNode(Context $context, CodeBase $code_base, Decl $node) : Func { // Parse the comment above the function to get // extra meta information about the function. $comment = Comment::fromStringInContext($node->docComment ?? '', $context); // @var Parameter[] // The list of parameters specified on the // function $parameter_list = Parameter::listFromNode($context, $code_base, $node->children['params']); // Add each parameter to the scope of the function foreach ($parameter_list as $parameter) { $context = $context->withScopeVariable($parameter); } // Create the skeleton function object from what // we know so far $func = new Func($context, (string) $node->name, new UnionType(), $node->flags ?? 0); // If the function is Analyzable, set the node so that // we can come back to it whenever we like and // rescan it $func->setNode($node); // Set the parameter list on the function $func->setParameterList($parameter_list); $func->setNumberOfRequiredParameters(array_reduce($parameter_list, function (int $carry, Parameter $parameter) : int { return $carry + ($parameter->isRequired() ? 1 : 0); }, 0)); $func->setNumberOfOptionalParameters(array_reduce($parameter_list, function (int $carry, Parameter $parameter) : int { return $carry + ($parameter->isOptional() ? 1 : 0); }, 0)); // Check to see if the comment specifies that the // function is deprecated $func->setIsDeprecated($comment->isDeprecated()); $func->setSuppressIssueList($comment->getSuppressIssueList()); // Take a look at function return types if ($node->children['returnType'] !== null) { // Get the type of the parameter $union_type = UnionType::fromNode($context, $code_base, $node->children['returnType']); $func->getUnionType()->addUnionType($union_type); } if ($comment->hasReturnUnionType()) { // See if we have a return type specified in the comment $union_type = $comment->getReturnType(); assert(!$union_type->hasSelfType(), "Function referencing self in {$context}"); $func->getUnionType()->addUnionType($union_type); } // Add params to local scope for user functions if (!$func->isInternal()) { $parameter_offset = 0; foreach ($func->getParameterList() as $i => $parameter) { if ($parameter->getUnionType()->isEmpty()) { // If there is no type specified in PHP, check // for a docComment with @param declarations. We // assume order in the docComment matches the // parameter order in the code if ($comment->hasParameterWithNameOrOffset($parameter->getName(), $parameter_offset)) { $comment_type = $comment->getParameterWithNameOrOffset($parameter->getName(), $parameter_offset)->getUnionType(); $parameter->getUnionType()->addUnionType($comment_type); } } // If there's a default value on the parameter, check to // see if the type of the default is cool with the // specified type. if ($parameter->hasDefaultValue()) { $default_type = $parameter->getDefaultValueType(); if (!$default_type->isEqualTo(NullType::instance()->asUnionType())) { if (!$default_type->isEqualTo(NullType::instance()->asUnionType()) && !$default_type->canCastToUnionType($parameter->getUnionType())) { Issue::maybeEmit($code_base, $context, Issue::TypeMismatchDefault, $node->lineno ?? 0, (string) $parameter->getUnionType(), $parameter->getName(), (string) $default_type); } $parameter->getUnionType()->addUnionType($default_type); } // If we have no other type info about a parameter, // just because it has a default value of null // doesn't mean that is its type. Any type can default // to null if ((string) $default_type === 'null' && !$parameter->getUnionType()->isEmpty()) { $parameter->getUnionType()->addType(NullType::instance()); } } ++$parameter_offset; } } return $func; }
/** * @param CodeBase $code_base * The code base within which we're operating * * @param $context $context * The context of the parser at the node for which we'd * like to determine a type * * @param Node|mixed $node * The node for which we'd like to determine its type * * @return UnionType * The UnionType associated with the given node * in the given Context within the given CodeBase * * @throws IssueException * An exception is thrown if we can't find a class for * the given type */ public static function unionTypeFromClassNode(CodeBase $code_base, Context $context, $node) : UnionType { // For simple nodes or very complicated nodes, // recurse if (!$node instanceof \ast\Node || $node->kind != \ast\AST_NAME) { return self::unionTypeFromNode($code_base, $context, $node); } $class_name = $node->children['name']; // Check to see if the name is fully qualified if (!($node->flags & \ast\flags\NAME_NOT_FQ)) { if (0 !== strpos($class_name, '\\')) { $class_name = '\\' . $class_name; } return UnionType::fromFullyQualifiedString($class_name); } if ('parent' === $class_name) { if (!$context->isInClassScope()) { throw new IssueException(Issue::fromType(Issue::ContextNotObject)($context->getFile(), $node->lineno ?? 0, [$class_name])); } $class = $context->getClassInScope($code_base); if ($class->isTrait()) { throw new IssueException(Issue::fromType(Issue::TraitParentReference)($context->getFile(), $node->lineno ?? 0, [(string) $context->getClassFQSEN()])); } if (!$class->hasParentClassFQSEN()) { throw new IssueException(Issue::fromType(Issue::ParentlessClass)($context->getFile(), $node->lineno ?? 0, [(string) $context->getClassFQSEN()])); } $parent_class_fqsen = $class->getParentClassFQSEN(); if (!$code_base->hasClassWithFQSEN($parent_class_fqsen)) { throw new IssueException(Issue::fromType(Issue::UndeclaredClass)($context->getFile(), $node->lineno ?? 0, [(string) $parent_class_fqsen])); } else { $parent_class = $code_base->getClassByFQSEN($parent_class_fqsen); return $parent_class->getUnionType(); } } return UnionType::fromStringInContext($class_name, $context); }
/** * @param Node $node * A node to check types on * * @return UnionType * The resulting type(s) of the binary operation */ public function visitBinaryAdd(Node $node) : UnionType { $left = UnionType::fromNode($this->context, $this->code_base, $node->children['left']); $right = UnionType::fromNode($this->context, $this->code_base, $node->children['right']); // fast-track common cases if ($left->isType(IntType::instance()) && $right->isType(IntType::instance())) { return IntType::instance()->asUnionType(); } // If both left and right are arrays, then this is array // concatenation. if ($left->isGenericArray() && $right->isGenericArray()) { if ($left->isEqualTo($right)) { return $left; } return ArrayType::instance()->asUnionType(); } if (($left->isType(IntType::instance()) || $left->isType(FloatType::instance())) && ($right->isType(IntType::instance()) || $right->isType(FloatType::instance()))) { return FloatType::instance()->asUnionType(); } $left_is_array = !empty($left->genericArrayElementTypes()) && empty($left->nonGenericArrayTypes()) || $left->isType(ArrayType::instance()); $right_is_array = !empty($right->genericArrayElementTypes()) && empty($right->nonGenericArrayTypes()) || $right->isType(ArrayType::instance()); if ($left_is_array && !$right->canCastToUnionType(ArrayType::instance()->asUnionType())) { Issue::maybeEmit($this->code_base, $this->context, Issue::TypeInvalidRightOperand, $node->lineno ?? 0); return new UnionType(); } elseif ($right_is_array && !$left->canCastToUnionType(ArrayType::instance()->asUnionType())) { Issue::maybeEmit($this->code_base, $this->context, Issue::TypeInvalidLeftOperand, $node->lineno ?? 0); return new UnionType(); } elseif ($left_is_array || $right_is_array) { // If it is a '+' and we know one side is an array // and the other is unknown, assume array return ArrayType::instance()->asUnionType(); } return new UnionType([IntType::instance(), FloatType::instance()]); }
/** * @param Context $context * The context in which the node appears * * @param CodeBase $code_base * * @param Node $node * An AST node representing a method * * @return Method * A Method representing the AST node in the * given context */ public static function fromNode(Context $context, CodeBase $code_base, Decl $node, FullyQualifiedMethodName $fqsen) : Method { // Create the skeleton method object from what // we know so far $method = new Method($context, (string) $node->name, new UnionType(), $node->flags ?? 0, $fqsen); // Parse the comment above the method to get // extra meta information about the method. $comment = Comment::fromStringInContext($node->docComment ?? '', $context); // @var Parameter[] // The list of parameters specified on the // method $parameter_list = Parameter::listFromNode($context, $code_base, $node->children['params']); // Add each parameter to the scope of the function foreach ($parameter_list as $parameter) { $method->getInternalScope()->addVariable($parameter); } // If the method is Analyzable, set the node so that // we can come back to it whenever we like and // rescan it $method->setNode($node); // Set the parameter list on the method $method->setParameterList($parameter_list); $method->setNumberOfRequiredParameters(array_reduce($parameter_list, function (int $carry, Parameter $parameter) : int { return $carry + ($parameter->isRequired() ? 1 : 0); }, 0)); $method->setNumberOfOptionalParameters(array_reduce($parameter_list, function (int $carry, Parameter $parameter) : int { return $carry + ($parameter->isOptional() ? 1 : 0); }, 0)); // Check to see if the comment specifies that the // method is deprecated $method->setIsDeprecated($comment->isDeprecated()); $method->setSuppressIssueList($comment->getSuppressIssueList()); if ($method->getIsMagicCall() || $method->getIsMagicCallStatic()) { $method->setNumberOfOptionalParameters(999); $method->setNumberOfRequiredParameters(0); } // Take a look at method return types if ($node->children['returnType'] !== null) { // Get the type of the parameter $union_type = UnionType::fromNode($context, $code_base, $node->children['returnType']); $method->getUnionType()->addUnionType($union_type); } if ($comment->hasReturnUnionType()) { // See if we have a return type specified in the comment $union_type = $comment->getReturnType(); if ($union_type->hasSelfType()) { // We can't actually figure out 'static' at this // point, but fill it in regardless. It will be partially // correct if ($context->isInClassScope()) { // n.b.: We're leaving the reference to self, static // or $this in the type because I'm guessing // it doesn't really matter. Apologies if it // ends up being an issue. $union_type->addUnionType($context->getClassFQSEN()->asUnionType()); } } $method->getUnionType()->addUnionType($union_type); } // Add params to local scope for user functions if (!$method->isInternal()) { $parameter_offset = 0; foreach ($method->getParameterList() as $i => $parameter) { if ($parameter->getUnionType()->isEmpty()) { // If there is no type specified in PHP, check // for a docComment with @param declarations. We // assume order in the docComment matches the // parameter order in the code if ($comment->hasParameterWithNameOrOffset($parameter->getName(), $parameter_offset)) { $comment_type = $comment->getParameterWithNameOrOffset($parameter->getName(), $parameter_offset)->getUnionType(); $parameter->getUnionType()->addUnionType($comment_type); } } // If there's a default value on the parameter, check to // see if the type of the default is cool with the // specified type. if ($parameter->hasDefaultValue()) { $default_type = $parameter->getDefaultValueType(); if (!$default_type->isEqualTo(NullType::instance()->asUnionType())) { if (!$default_type->isEqualTo(NullType::instance()->asUnionType()) && !$default_type->canCastToUnionType($parameter->getUnionType())) { Issue::maybeEmit($code_base, $context, Issue::TypeMismatchDefault, $node->lineno ?? 0, (string) $parameter->getUnionType(), $parameter->getName(), (string) $default_type); } $parameter->getUnionType()->addUnionType($default_type); } // If we have no other type info about a parameter, // just because it has a default value of null // doesn't mean that is its type. Any type can default // to null if ((string) $default_type === 'null' && !$parameter->getUnionType()->isEmpty()) { $parameter->getUnionType()->addType(NullType::instance()); } } ++$parameter_offset; } } return $method; }
/** * @param Node $node * A node of the type indicated by the method name that we'd * like to figure out the type that it produces. * * @return string * The class name represented by the given call */ public function visitMethodCall(Node $node) : string { // Get the type returned by the first method // call. $union_type = UnionType::fromNode($this->context, $this->code_base, $node); // Find the subset of types that are viable // classes $viable_class_types = $union_type->nonNativeTypes()->nonGenericArrayTypes(); // If there are no non-native types, give up if ($viable_class_types->isEmpty()) { return ''; } // Return the first non-native type in the // list and hope its a class return (string) $viable_class_types->head(); }
/** * @param Node $node * A node to check types on * * @return UnionType * The resulting type(s) of the binary operation */ public function visitBinaryAdd(Node $node) : UnionType { $left = UnionType::fromNode($this->context, $this->code_base, $node->children['left']); $right = UnionType::fromNode($this->context, $this->code_base, $node->children['right']); // fast-track common cases if ($left->isType(IntType::instance()) && $right->isType(IntType::instance())) { return IntType::instance()->asUnionType(); } if (($left->isType(IntType::instance()) || $left->isType(FloatType::instance())) && ($right->isType(IntType::instance()) || $right->isType(FloatType::instance()))) { return FloatType::instance()->asUnionType(); } $left_is_array = !empty($left->genericArrayElementTypes()) && empty($left->nonGenericArrayTypes()); $right_is_array = !empty($right->genericArrayElementTypes()) && empty($right->nonGenericArrayTypes()); if ($left_is_array && !$right->canCastToUnionType(ArrayType::instance()->asUnionType())) { Log::err(Log::ETYPE, "invalid operator: left operand is array and right is not", $this->context->getFile(), $node->lineno); return new UnionType(); } else { if ($right_is_array && !$left->canCastToUnionType(ArrayType::instance()->asUnionType())) { Log::err(Log::ETYPE, "invalid operator: right operand is array and left is not", $this->context->getFile(), $node->lineno); return new UnionType(); } else { if ($left_is_array || $right_is_array) { // If it is a '+' and we know one side is an array // and the other is unknown, assume array return ArrayType::instance()->asUnionType(); } } } return new UnionType([IntType::instance(), FloatType::instance()]); }
/** * Check to see if signatures match * * @return void */ public static function analyzeComposition(CodeBase $code_base, Clazz $class) { // Get the Class's FQSEN $fqsen = $class->getFQSEN(); // Get the list of all inherited classes. $inherited_class_list = $class->getInheritedClassList($code_base); // No chance of failed composition if we don't inherit from // lots of stuff. if (count($inherited_class_list) < 2) { return; } // For each property, find out every inherited class that defines it // and check to see if the types line up. foreach ($class->getPropertyList($code_base) as $property) { try { $property_union_type = $property->getUnionType(); } catch (IssueException $exception) { $property_union_type = new UnionType(); } // Check for that property on each inherited // class/trait/interface foreach ($inherited_class_list as $inherited_class) { // Skip any classes/traits/interfaces not defining that // property if (!$inherited_class->hasPropertyWithName($code_base, $property->getName())) { continue; } // We don't call `getProperty` because that will create // them in some circumstances. $inherited_property_map = $inherited_class->getPropertyMap($code_base); if (!isset($inherited_property_map[$property->getName()])) { continue; } // Get the inherited property $inherited_property = $inherited_property_map[$property->getName()]; // Figure out if this property type can cast to the // inherited definition's type. $can_cast = $property_union_type->canCastToExpandedUnionType($inherited_property->getUnionType(), $code_base); if ($can_cast) { continue; } // Don't emit an issue if the property suppresses the issue if ($property->hasSuppressIssue(Issue::IncompatibleCompositionProp)) { continue; } Issue::maybeEmit($code_base, $property->getContext(), Issue::IncompatibleCompositionProp, $property->getFileRef()->getLineNumberStart(), (string) $class->getFQSEN(), (string) $inherited_class->getFQSEN(), $property->getName(), (string) $class->getFQSEN(), $class->getFileRef()->getFile(), $class->getFileRef()->getLineNumberStart()); } } // TODO: This has too much overlap with PhanParamSignatureMismatch // and we should figure out how to merge it. /* $method_map = $code_base->getMethodMapByFullyQualifiedClassName($fqsen); // For each method, find out every inherited class that defines it // and check to see if the types line up. foreach ($method_map as $i => $method) { $method_union_type = $method->getUnionType(); // We don't need to analyze constructors for signature // compatibility if ($method->getName() == '__construct') { continue; } // Get the method parameter list // Check for that method on each inherited // class/trait/interface foreach ($inherited_class_list as $inherited_class) { // Skip anything that doesn't define this method if (!$inherited_class->hasMethodWithName($code_base, $method->getName())) { continue; } $inherited_method = $inherited_class->getMethodByName($code_base, $method->getName()); if ($method == $inherited_method) { continue; } // Figure out if this method return type can cast to the // inherited definition's return type. $is_compatible = $method_union_type->canCastToExpandedUnionType( $inherited_method->getUnionType(), $code_base ); $inherited_method_parameter_map = $inherited_method->getParameterList(); // Figure out if all of the parameter types line up foreach ($method->getParameterList() as $i => $parameter) { $is_compatible = ( $is_compatible && isset($inherited_method_parameter_map[$i]) && $parameter->getUnionType()->canCastToExpandedUnionType( ($inherited_method_parameter_map[$i])->getUnionType(), $code_base ) ); } if ($is_compatible) { continue; } // Don't emit an issue if the method suppresses the issue if ($method->hasSuppressIssue(Issue::IncompatibleCompositionMethod)) { continue; } Issue::maybeEmit( $code_base, $method->getContext(), Issue::IncompatibleCompositionMethod, $method->getFileRef()->getLineNumberStart(), (string)$method, (string)$inherited_method, $inherited_method->getFileRef()->getFile(), $inherited_method->getFileRef()->getLineNumberStart() ); } } */ }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitProp(Node $node) : Context { $property_name = $node->children['prop']; // Things like $foo->$bar if (!is_string($property_name)) { return $this->context; } assert(is_string($property_name), "Property must be string"); try { $class_list = (new ContextNode($this->code_base, $this->context, $node->children['expr']))->getClassList(); } catch (CodeBaseException $exception) { // This really shouldn't happen since the code // parsed cleanly. This should fatal. // throw $exception; return $this->context; } catch (\Exception $exception) { // If we can't figure out what kind of a class // this is, don't worry about it return $this->context; } foreach ($class_list as $clazz) { // Check to see if this class has the property or // a setter if (!$clazz->hasPropertyWithName($this->code_base, $property_name)) { if (!$clazz->hasMethodWithName($this->code_base, '__set')) { continue; } } try { $property = $clazz->getPropertyByNameInContext($this->code_base, $property_name, $this->context); } catch (IssueException $exception) { Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance()); return $this->context; } if (!$this->right_type->canCastToExpandedUnionType($property->getUnionType(), $this->code_base)) { $this->emitIssue(Issue::TypeMismatchProperty, $node->lineno ?? 0, (string) $this->right_type, "{$clazz->getFQSEN()}::{$property->getName()}", (string) $property->getUnionType()); return $this->context; } else { // If we're assigning to an array element then we don't // know what the constitutation of the parameter is // outside of the scope of this assignment, so we add to // its union type rather than replace it. if ($this->is_dim_assignment) { $property->getUnionType()->addUnionType($this->right_type); } } // After having checked it, add this type to it $property->getUnionType()->addUnionType($this->right_type); return $this->context; } $std_class_fqsen = FullyQualifiedClassName::getStdClassFQSEN(); if (Config::get()->allow_missing_properties || !empty($class_list) && $class_list[0]->getFQSEN() == $std_class_fqsen) { try { // Create the property $property = (new ContextNode($this->code_base, $this->context, $node))->getOrCreateProperty($property_name); $property->getUnionType()->addUnionType($this->right_type); } catch (\Exception $exception) { // swallow it } } elseif (!empty($class_list)) { $this->emitIssue(Issue::UndeclaredProperty, $node->lineno ?? 0, "{$class_list[0]->getFQSEN()}->{$property_name}"); } else { // If we hit this part, we couldn't figure out // the class, so we ignore the issue } return $this->context; }
/** * @return bool * True if this doc block contains a (at)return * directive specifying a type. */ public function hasReturnUnionType() : bool { return !empty($this->return_union_type) && !$this->return_union_type->isEmpty(); }
/** * @return UnionType|null * Returns UnionType (Possible with empty set) if and only if isHardcodedGlobalVariableWithName is true. * Returns null otherwise. */ public static function getUnionTypeOfHardcodedGlobalVariableWithName(string $name, Context $context) { if (array_key_exists($name, self::_BUILTIN_SUPERGLOBAL_TYPES)) { // More efficient than using context. return UnionType::fromFullyQualifiedString(self::_BUILTIN_SUPERGLOBAL_TYPES[$name]); } if (array_key_exists($name, Config::get()->globals_type_map) || in_array($name, Config::get()->runkit_superglobals)) { $type_string = Config::get()->globals_type_map[$name] ?? ''; return UnionType::fromStringInContext($type_string, $context); } return null; }
/** * @param CodeBase * The code base to use in order to find super classes, etc. * * @param $recursion_depth * This thing has a tendency to run-away on me. This tracks * how bad I messed up by seeing how far the expanded types * go * * @return UnionType * Expands class types to all inherited classes returning * a superset of this type. */ public function asExpandedTypes(CodeBase $code_base, int $recursion_depth = 0) : UnionType { return $this->memoize(__METHOD__, function () use($code_base, $recursion_depth) : UnionType { assert($recursion_depth < 10, "Recursion has gotten out of hand for type {$this}"); if ($this->isNativeType()) { return $this->asUnionType(); } $union_type = $this->asUnionType(); $class_fqsen = $this->isGenericArray() ? $this->genericArrayElementType()->asFQSEN() : $this->asFQSEN(); if (!$code_base->hasClassWithFQSEN($class_fqsen)) { return $union_type; } $clazz = $code_base->getClassByFQSEN($class_fqsen); $union_type->addUnionType($this->isGenericArray() ? $clazz->getUnionType()->asGenericArrayTypes() : $clazz->getUnionType()); // Resurse up the tree to include all types $recursive_union_type = new UnionType(); foreach ($union_type->getTypeList() as $clazz_type) { if ((string) $clazz_type != (string) $this) { $recursive_union_type->addUnionType($clazz_type->asExpandedTypes($code_base, $recursion_depth + 1)); } else { $recursive_union_type->addType($clazz_type); } } return $recursive_union_type; }); }
/** * @return null */ public function addTraitFQSEN(FQSEN $fqsen) { $this->trait_fqsen_list[] = $fqsen; // Add the trait to the union type of this class $this->getUnionType()->addUnionType(UnionType::fromFullyQualifiedString((string) $fqsen)); }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitForeach(Node $node) : Context { if ($node->children['value']->kind == \ast\AST_LIST) { foreach ($node->children['value']->children as $child_node) { $variable = Variable::fromNodeInContext($child_node, $this->context, $this->code_base, false); $this->context->addScopeVariable($variable); } // Otherwise, read the value as regular variable and // add it to the scope } else { // Create a variable for the value $variable = Variable::fromNodeInContext($node->children['value'], $this->context, $this->code_base, false); // Get the type of the node from the left side $type = UnionType::fromNode($this->context, $this->code_base, $node->children['expr']); // Filter out the non-generic types of the // expression $non_generic_type = $type->asNonGenericTypes(); // If we were able to figure out the type and its // a generic type, then set its element types as // the type of the variable if (!$non_generic_type->isEmpty()) { $variable->setUnionType($non_generic_type); } // Add the variable to the scope $this->context->addScopeVariable($variable); } // If there's a key, make a variable out of that too if (!empty($node->children['key'])) { if ($node->children['key'] instanceof \ast\Node && $node->children['key']->kind == \ast\AST_LIST) { Log::err(Log::EFATAL, "Can't use list() as a key element - aborting", $this->context->getFile(), $node->lineno); } $variable = Variable::fromNodeInContext($node->children['key'], $this->context, $this->code_base, false); $this->context->addScopeVariable($variable); } // Note that we're not creating a new scope, just // adding variables to the existing scope return $this->context; }
/** * Visit a node with kind `\ast\AST_PROP_DECL` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitPropDecl(Node $node) : Context { // Bomb out if we're not in a class context $clazz = $this->getContextClass(); // Get a comment on the property declaration $comment = Comment::fromStringInContext($node->children[0]->docComment ?? '', $this->context); foreach ($node->children ?? [] as $i => $child_node) { // Ignore children which are not property elements if (!$child_node || $child_node->kind != \ast\AST_PROP_ELEM) { continue; } // If something goes wrong will getting the type of // a property, we'll store it as a future union // type and try to figure it out later $future_union_type = null; try { // Get the type of the default $union_type = UnionType::fromNode($this->context, $this->code_base, $child_node->children['default'], false); } catch (IssueException $exception) { $future_union_type = new FutureUnionType($this->code_base, $this->context, $child_node->children['default']); $union_type = new UnionType(); } // Don't set 'null' as the type if thats the default // given that its the default default. if ($union_type->isType(NullType::instance())) { $union_type = new UnionType(); } $property_name = $child_node->children['name']; assert(is_string($property_name), 'Property name must be a string. ' . 'Got ' . print_r($property_name, true) . ' at ' . $this->context); $property = new Property(clone $this->context->withLineNumberStart($child_node->lineno ?? 0), is_string($child_node->children['name']) ? $child_node->children['name'] : '_error_', $union_type, $node->flags ?? 0); $property->setFQSEN(FullyQualifiedPropertyName::make($clazz->getFQSEN(), $property->getName())); // Add the property to the class $clazz->addProperty($this->code_base, $property); $property->setSuppressIssueList($comment->getSuppressIssueList()); // Look for any @var declarations if ($variable = $comment->getVariableList()[$i] ?? null) { if ((string) $union_type != 'null' && !$union_type->canCastToUnionType($variable->getUnionType())) { $this->emitIssue(Issue::TypeMismatchProperty, $child_node->lineno ?? 0, (string) $union_type, (string) $property->getFQSEN(), (string) $variable->getUnionType()); } // Set the declared type to the doc-comment type and add // |null if the default value is null $property->getUnionType()->addUnionType($variable->getUnionType()); } // Wait until after we've added the (at)var type // before setting the future so that calling // $property->getUnionType() doesn't force the // future to be reified. if (!empty($future_union_type)) { $property->setFutureUnionType($future_union_type); } } return $this->context; }
/** * @param array * A map from column name to value * * @return Model * An instance of the model derived from row data */ public static function fromRow(array $row) : Method { list($scope, $name) = explode('|', $row['scope_name']); return new Method(new MethodElement(unserialize(base64_decode($row['context'])), $row['name'], UnionType::fromFullyQualifiedString($row['type']), (int) $row['flags'], $row['number_of_required_parameters'], $row['number_of_optional_parameters'], (bool) $row['is_dynamic']), $scope, $name); }
/** * 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, Method $method, Node $node) { if (Database::isEnabled()) { // Store the call to the method so we can track // dependencies later (new CalledBy((string) $method->getFQSEN(), $this->context))->write(Database::get()); } // Create variables for any pass-by-reference // parameters $argument_list = $node->children['args']; foreach ($argument_list->children as $i => $argument) { $parameter = $method->getParameterList()[$i] ?? null; 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 = AST::getOrCreateVariableFromNodeInContext($argument, $this->context, $this->code_base); } else { if ($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 = AST::getOrCreatePropertyFromNodeInContext($argument->children['prop'], $argument, $this->context, $this->code_base); } catch (CodeBaseException $exception) { Log::err(Log::EUNDEF, $exception->getMessage(), $this->context->getFile(), $node->lineno); } catch (NodeException $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) { $parameter = $method->getParameterList()[$i] ?? null; if (!$parameter) { continue; } // 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 = AST::getOrCreateVariableFromNodeInContext($argument, $this->context, $this->code_base); } else { if ($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 = AST::getOrCreatePropertyFromNodeInContext($argument->children['prop'], $argument, $this->context, $this->code_base); } catch (CodeBaseException $exception) { Log::err(Log::EUNDEF, $exception->getMessage(), $this->context->getFile(), $node->lineno); } catch (NodeException $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->getUnionType()); } } } // 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(); foreach ($argument_list->children as $i => $argument) { $parameter = $method->getParameterList()[$i] ?? null; if (!$parameter) { continue; } // If the parameter has no type, pass the // argument's type to it if ($parameter->getUnionType()->isEmpty()) { $has_argument_parameter_mismatch = true; $argument_type = UnionType::fromNode($this->context, $this->code_base, $argument); // 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->getContext()->isInternal()) { // Clone the parameter in the original // parameter list so we can reset it // later $original_parameter_list[$i] = clone $parameter; // 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->getUnionType()->addUnionType($argument_type); } } } // 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->getContext()->isInternal() && (!$this->context->isMethodScope() || $method->getFQSEN() !== $this->context->getMethodFQSEN())) { $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); }
/** * @param UnionType $target * A type to check to see if this can cast to it * * @return bool * True if this type is allowed to cast to the given type * i.e. int->float is allowed while float->int is not. * * @see \Phan\Deprecated\Pass2::type_check * Formerly 'function type_check' */ public function canCastToUnionType(UnionType $target) : bool { // Fast-track most common cases first // If either type is unknown, we can't call it // a success if ($this->isEmpty() || $target->isEmpty()) { return true; } // T === T if ($this->isEqualTo($target)) { return true; } if (Config::get()->null_casts_as_any_type) { // null <-> null if ($this->isType(NullType::instance()) || $target->isType(NullType::instance())) { return true; } } // mixed <-> mixed if ($target->hasType(MixedType::instance()) || $this->hasType(MixedType::instance())) { return true; } // int -> float if ($this->isType(IntType::instance()) && $target->isType(FloatType::instance())) { return true; } // Check conversion on the cross product of all // type combinations and see if any can cast to // any. foreach ($this->getTypeList() as $source_type) { if (empty($source_type)) { continue; } foreach ($target->getTypeList() as $target_type) { if (empty($target_type)) { continue; } if ($source_type->canCastToType($target_type)) { return true; } } } // Only if no source types can be cast to any target // types do we say that we cannot perform the cast return false; }
/** * @param CodeBase * The code base to use in order to find super classes, etc. * * @param $recursion_depth * This thing has a tendency to run-away on me. This tracks * how bad I messed up by seeing how far the expanded types * go * * @return UnionType * Expands class types to all inherited classes returning * a superset of this type. */ public function asExpandedTypes(CodeBase $code_base, int $recursion_depth = 0) : UnionType { return $this->memoize(__METHOD__, function () use($code_base, $recursion_depth) : UnionType { // We're going to assume that if the type hierarchy // is taller than some value we probably messed up // and should bail out. assert($recursion_depth < 20, "Recursion has gotten out of hand for type {$this}"); if ($this->isNativeType()) { return $this->asUnionType(); } $union_type = $this->asUnionType(); $class_fqsen = $this->isGenericArray() ? $this->genericArrayElementType()->asFQSEN() : $this->asFQSEN(); if (!$code_base->hasClassWithFQSEN($class_fqsen)) { return $union_type; } $clazz = $code_base->getClassByFQSEN($class_fqsen); $union_type->addUnionType($this->isGenericArray() ? $clazz->getUnionType()->asGenericArrayTypes() : $clazz->getUnionType()); // Resurse up the tree to include all types $recursive_union_type = new UnionType(); foreach ($union_type->getTypeSet() as $clazz_type) { if ((string) $clazz_type != (string) $this) { $recursive_union_type->addUnionType($clazz_type->asExpandedTypes($code_base, $recursion_depth + 1)); } else { $recursive_union_type->addType($clazz_type); } } return $recursive_union_type; }); }
/** * Check to see if the given Clazz is a duplicate * * @param Method $method * The method we're analyzing arguments for * * @param Node $node * The node holding the method call we're looking at * * @param Context $context * The context in which we see the call * * @param CodeBase $code_base * * @return null * * @see \Phan\Deprecated\Pass2::arg_check * Formerly `function arg_check` */ private static function analyzeInternalArgumentType(Method $method, Node $node, Context $context, CodeBase $code_base) { $arglist = $node->children['args']; $argcount = count($arglist->children); switch ($method->getName()) { case 'join': case 'implode': // (string glue, array pieces), // (array pieces, string glue) or // (array pieces) if ($argcount == 1) { self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#1(pieces) is %s but {$method->getFQSEN()}() takes array when passed only 1 arg"); return; } else { if ($argcount == 2) { $arg1_type = UnionType::fromNode($context, $code_base, $arglist->children[0]); $arg2_type = UnionType::fromNode($context, $code_base, $arglist->children[1]); if ((string) $arg1_type == 'array') { if (!$arg1_type->canCastToUnionType(StringType::instance()->asUnionType())) { Log::err(Log::EPARAM, "arg#2(glue) is {$arg2_type} but {$method->getFQSEN()}() takes string when arg#1 is array", $context->getFile(), $context->getLineNumberStart()); } } else { if ((string) $arg1_type == 'string') { if (!$arg2_type->canCastToUnionType(ArrayType::instance()->asUnionType())) { Log::err(Log::EPARAM, "arg#2(pieces) is {$arg2_type} but {$method->getFQSEN()}() takes array when arg#1 is string", $context->getFile(), $context->getLineNumberStart()); } } } return; } } // Any other arg counts we will let the regular // checks handle break; case 'array_udiff': case 'array_diff_uassoc': case 'array_uintersect_assoc': case 'array_intersect_ukey': if ($argcount < 3) { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$method->getFQSEN()}() which requires {$method->getNumberOfRequiredParameters()} arg(s)", $context->getFile(), $context->getLineNumberStart()); return; } self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 1], $context, $code_base, CallableType::instance()->asUnionType(), "The last argument to {$method->getFQSEN()} must be a callable"); for ($i = 0; $i < $argcount - 1; $i++) { self::analyzeNodeUnionTypeCast($arglist->children[$i], $context, $code_base, CallableType::instance()->asUnionType(), "arg#" . ($i + 1) . " is %s but {$method->getFQSEN()}() takes array"); } return; case 'array_diff_uassoc': case 'array_uintersect_uassoc': if ($argcount < 4) { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$method->getFQSEN()}() which requires {$method->getNumberOfRequiredParameters()} arg(s)", $context->getFile(), $context->getLineNumberStart()); return; } // The last 2 arguments must be a callable and there // can be a variable number of arrays before it self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 1], $context, $code_base, CallableType::instance()->asUnionType(), "The last argument to {$method->getFQSEN()} must be a callable"); self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 2], $context, $code_base, CallableType::instance()->asUnionType(), "The second last argument to {$method->getFQSEN()} must be a callable"); for ($i = 0; $i < $argcount - 2; $i++) { self::analyzeNodeUnionTypeCast($arglist->children[$i], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#" . ($i + 1) . " is %s but {$method->getFQSEN()}() takes array"); } return; case 'strtok': // (string str, string token) or (string token) if ($argcount == 1) { // If we have just one arg it must be a string token self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#1(token) is %s but {$method->getFQSEN()}() takes string when passed only one arg"); } // The arginfo check will handle the other case break; case 'min': case 'max': if ($argcount == 1) { // If we have just one arg it must be an array if (!self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#1(values) is %s but {$method->getFQSEN()}() takes array when passed only one arg")) { return; } } // The arginfo check will handle the other case break; default: break; } }