/**
  * @return UnionType|null
  * Get the UnionType from a future union type defined
  * on this object or null if there is no future
  * union type.
  */
 public function getFutureUnionType()
 {
     if (empty($this->future_union_type)) {
         return null;
     }
     // null out the future_union_type before
     // we compute it to avoid unbounded
     // recursion
     $future_union_type = $this->future_union_type;
     $this->future_union_type = null;
     $union_type = $future_union_type->get();
     // 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();
     }
     return $union_type;
 }
Esempio n. 2
0
File: Type.php Progetto: etsy/phan
 /**
  * @param string $string
  * A string representing a type
  *
  * @param Context $context
  * The context in which the type string was
  * found
  *
  * @return Type
  * Parse a type from the given string
  */
 public static function fromStringInContext(string $string, Context $context) : Type
 {
     assert($string !== '', "Type cannot be empty");
     // Extract the namespace, type and parameter type name list
     $tuple = self::typeStringComponents($string);
     $namespace = $tuple->_0;
     $type_name = $tuple->_1;
     $template_parameter_type_name_list = $tuple->_2;
     // Map the names of the types to actual types in the
     // template parameter type list
     $template_parameter_type_list = array_map(function (string $type_name) use($context) {
         return Type::fromStringInContext($type_name, $context)->asUnionType();
     }, $template_parameter_type_name_list);
     // @var bool
     // True if this type name if of the form 'C[]'
     $is_generic_array_type = self::isGenericArrayString($type_name);
     // If this is a generic array type, get the name of
     // the type of each element
     $non_generic_array_type_name = $type_name;
     if ($is_generic_array_type && false !== ($pos = strrpos($type_name, '[]'))) {
         $non_generic_array_type_name = substr($type_name, 0, $pos);
     }
     // Check to see if the type name is mapped via
     // a using clause.
     //
     // Gotta check this before checking for native types
     // because there are monsters out there that will
     // remap the names via things like `use \Foo\String`.
     $non_generic_partially_qualified_array_type_name = $non_generic_array_type_name;
     if ($namespace) {
         $non_generic_partially_qualified_array_type_name = $namespace . '\\' . $non_generic_partially_qualified_array_type_name;
     }
     if ($context->hasNamespaceMapFor(\ast\flags\USE_NORMAL, $non_generic_partially_qualified_array_type_name)) {
         $fqsen = $context->getNamespaceMapFor(\ast\flags\USE_NORMAL, $non_generic_partially_qualified_array_type_name);
         if ($is_generic_array_type) {
             return GenericArrayType::fromElementType(Type::make($fqsen->getNamespace(), $fqsen->getName(), $template_parameter_type_list));
         }
         return Type::make($fqsen->getNamespace(), $fqsen->getName(), $template_parameter_type_list);
     }
     // If this was a fully qualified type, we're all
     // set
     if (!empty($namespace) && 0 === strpos($namespace, '\\')) {
         return self::make($namespace, $type_name, $template_parameter_type_list);
     }
     if ($is_generic_array_type && self::isNativeTypeString($type_name)) {
         return self::fromInternalTypeName($type_name);
     } else {
         // Check to see if its a builtin type
         switch (strtolower(self::canonicalNameFromName($type_name))) {
             case 'array':
                 return \Phan\Language\Type\ArrayType::instance();
             case 'bool':
                 return \Phan\Language\Type\BoolType::instance();
             case 'callable':
                 return \Phan\Language\Type\CallableType::instance();
             case 'float':
                 return \Phan\Language\Type\FloatType::instance();
             case 'int':
                 return \Phan\Language\Type\IntType::instance();
             case 'mixed':
                 return \Phan\Language\Type\MixedType::instance();
             case 'null':
                 return \Phan\Language\Type\NullType::instance();
             case 'object':
                 return \Phan\Language\Type\ObjectType::instance();
             case 'resource':
                 return \Phan\Language\Type\ResourceType::instance();
             case 'string':
                 return \Phan\Language\Type\StringType::instance();
             case 'void':
                 return \Phan\Language\Type\VoidType::instance();
             case 'static':
                 return \Phan\Language\Type\StaticType::instance();
         }
     }
     // Things like `self[]` or `$this[]`
     if ($is_generic_array_type && self::isSelfTypeString($non_generic_array_type_name) && $context->isInClassScope()) {
         // Callers of this method should be checking on their own
         // to see if this type is a reference to 'parent' and
         // dealing with it there. We don't want to have this
         // method be dependent on the code base
         assert('parent' !== $non_generic_array_type_name, __METHOD__ . " does not know how to handle the type name 'parent'");
         return GenericArrayType::fromElementType(static::fromFullyQualifiedString((string) $context->getClassFQSEN()));
     }
     // If this is a type referencing the current class
     // in scope such as 'self' or 'static', return that.
     if (self::isSelfTypeString($type_name) && $context->isInClassScope()) {
         // Callers of this method should be checking on their own
         // to see if this type is a reference to 'parent' and
         // dealing with it there. We don't want to have this
         // method be dependent on the code base
         assert('parent' !== $type_name, __METHOD__ . " does not know how to handle the type name 'parent'");
         return static::fromFullyQualifiedString((string) $context->getClassFQSEN());
     }
     // Merge the current namespace with the given relative
     // namespace
     if (!empty($context->getNamespace()) && !empty($namespace)) {
         $namespace = $context->getNamespace() . '\\' . $namespace;
     } else {
         if (!empty($context->getNamespace())) {
             $namespace = $context->getNamespace();
         } else {
             $namespace = '\\' . $namespace;
         }
     }
     // Attach the context's namespace to the type name
     return self::make($namespace, $type_name, $template_parameter_type_list);
 }
Esempio n. 3
0
 /**
  * @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;
 }
Esempio n. 4
0
 public function visitTry(Node $node) : Context
 {
     // Get the list of scopes for each branch of the
     // conditional
     $scope_list = array_map(function (Context $context) {
         return $context->getScope();
     }, $this->child_context_list);
     // The 0th scope is the scope from Try
     $try_scope = $scope_list[0];
     $catch_scope_list = [];
     foreach ($node->children['catches'] ?? [] as $i => $catch_node) {
         $catch_scope_list[] = $scope_list[$i + 1];
     }
     // Merge in the types for any variables found in a catch.
     foreach ($try_scope->getVariableMap() as $variable_name => $variable) {
         foreach ($catch_scope_list as $catch_scope) {
             // Merge types if try and catch have a variable in common
             if ($catch_scope->hasLocalVariableWithName($variable_name)) {
                 $catch_variable = $catch_scope->getLocalVariableWithName($variable_name);
                 $variable->getUnionType()->addUnionType($catch_variable->getUnionType());
             }
         }
     }
     // Look for variables that exist in catch, but not try
     foreach ($catch_scope_list as $catch_scope) {
         foreach ($catch_scope->getVariableMap() as $variable_name => $variable) {
             if (!$try_scope->hasLocalVariableWithName($variable_name)) {
                 // Note that it can be null
                 $variable->getUnionType()->addType(NullType::instance());
                 // Add it to the try scope
                 $try_scope->addVariable($variable);
             }
         }
     }
     // If we have a finally, overwite types for each
     // element
     if (!empty($node->children['finallyStmts']) || !empty($node->children['finally'])) {
         $finally_scope = $scope_list[count($scope_list) - 1];
         foreach ($try_scope->getVariableMap() as $variable_name => $variable) {
             if ($finally_scope->hasLocalVariableWithName($variable_name)) {
                 $finally_variable = $finally_scope->getLocalVariableWithName($variable_name);
                 // Overwrite the variable with the type from the
                 // finally
                 if (!$finally_variable->getUnionType()->isEmpty()) {
                     $variable->setUnionType($finally_variable->getUnionType());
                 }
             }
         }
         // Look for variables that exist in finally, but not try
         foreach ($finally_scope->getVariableMap() as $variable_name => $variable) {
             if (!$try_scope->hasLocalVariableWithName($variable_name)) {
                 $try_scope->addVariable($variable);
             }
         }
     }
     // Return the context of the try with the types of
     // variables within its scope limited appropriately
     return $this->child_context_list[0];
 }
Esempio n. 5
0
 /**
  * @param string $string
  * A string representing a type
  *
  * @param Context $context
  * The context in which the type string was
  * found
  *
  * @return Type
  * Parse a type from the given string
  */
 public static function fromStringInContext(string $string, Context $context) : Type
 {
     assert($string !== '', "Type cannot be empty in {$context}");
     $namespace = null;
     // Extract the namespace if the type string is
     // fully-qualified
     if ('\\' === $string[0]) {
         list($namespace, $string) = self::namespaceAndTypeFromString($string);
     }
     $type_name = strtolower($string);
     // Check to see if the type name is mapped via
     // a using clause.
     //
     // Gotta check this before checking for native types
     // because there are monsters out there that will
     // remap the names via things like `use \Foo\String`.
     if ($context->hasNamespaceMapFor(T_CLASS, $type_name)) {
         $fqsen = $context->getNamespaceMapFor(T_CLASS, $type_name);
         return new Type($fqsen->getNamespace(), $fqsen->getName());
     }
     // If this was a fully qualified type, we're all
     // set
     if (!empty($namespace)) {
         return self::fromNamespaceAndName($namespace, $type_name);
     }
     // Check to see if its a builtin type
     switch (self::canonicalNameFromName($type_name)) {
         case 'array':
             return \Phan\Language\Type\ArrayType::instance();
         case 'bool':
             return \Phan\Language\Type\BoolType::instance();
         case 'callable':
             return \Phan\Language\Type\CallableType::instance();
         case 'float':
             return \Phan\Language\Type\FloatType::instance();
         case 'int':
             return \Phan\Language\Type\IntType::instance();
         case 'mixed':
             return \Phan\Language\Type\MixedType::instance();
         case 'null':
             return \Phan\Language\Type\NullType::instance();
         case 'object':
             return \Phan\Language\Type\ObjectType::instance();
         case 'resource':
             return \Phan\Language\Type\ResourceType::instance();
         case 'string':
             return \Phan\Language\Type\StringType::instance();
         case 'void':
             return \Phan\Language\Type\VoidType::instance();
     }
     // If this is a type referencing the current class
     // in scope such as 'self' or 'static', return that.
     if (self::isSelfTypeString($type_name) && $context->isInClassScope()) {
         return static::fromFullyQualifiedString((string) $context->getClassFQSEN());
     }
     // Attach the context's namespace to the type name
     return self::fromNamespaceAndName($context->getNamespace() ?: '\\', $type_name);
 }
Esempio n. 6
0
 /**
  * Visit a node with kind `\ast\AST_DIM`
  *
  * @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 UnionType
  * The set of types that are possibly produced by the
  * given node
  */
 public function visitDim(Node $node) : UnionType
 {
     $union_type = self::unionTypeFromNode($this->code_base, $this->context, $node->children['expr']);
     if ($union_type->isEmpty()) {
         return $union_type;
     }
     // Figure out what the types of accessed array
     // elements would be
     $generic_types = $union_type->genericArrayElementTypes();
     // If we have generics, we're all set
     if (!$generic_types->isEmpty()) {
         return $generic_types;
     }
     // If the only type is null, we don't know what
     // accessed items will be
     if ($union_type->isType(NullType::instance())) {
         return new UnionType();
     }
     $element_types = new UnionType();
     // You can access string characters via array index,
     // so we'll add the string type to the result if we're
     // indexing something that could be a string
     if ($union_type->isType(StringType::instance()) || $union_type->canCastToUnionType(StringType::instance()->asUnionType())) {
         $element_types->addType(StringType::instance());
     }
     // array offsets work on strings, unfortunately
     // Double check that any classes in the type don't
     // have ArrayAccess
     $array_access_type = Type::fromNamespaceAndName('\\', 'ArrayAccess');
     // Hunt for any types that are viable class names and
     // see if they inherit from ArrayAccess
     foreach ($union_type->getTypeList() as $type) {
         if ($type->isNativeType()) {
             continue;
         }
         $class_fqsen = FullyQualifiedClassName::fromType($type);
         // If we can't find the class, the type probably
         // wasn't a class.
         if (!$this->code_base->hasClassWithFQSEN($class_fqsen)) {
             continue;
         }
         $class = $this->code_base->getClassByFQSEN($class_fqsen);
         // If the class has type ArrayAccess, it can be indexed
         // as if it were an array. That being said, we still don't
         // know the types of the elements, but at least we don't
         // error out.
         if ($class->getUnionType()->hasType($array_access_type)) {
             return $element_types;
         }
     }
     if ($element_types->isEmpty()) {
         Log::err(Log::ETYPE, "Suspicious array access to {$union_type}", $this->context->getFile(), $node->lineno);
     }
     return $element_types;
 }
Esempio n. 7
0
 /**
  * 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;
 }
Esempio n. 8
0
 /**
  * @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());
         return $alternate_function;
     }, $map_list);
 }
Esempio n. 9
0
 /**
  * Look at elements of the form `is_array($v)` and modify
  * the type of the variable.
  *
  * @param Node $node
  * A node to parse
  *
  * @return Context
  * A new or an unchanged context resulting from
  * parsing the node
  */
 public function visitCall(Node $node) : Context
 {
     // Only look at things of the form
     // `is_string($variable)`
     if (count($node->children['args']->children) !== 1 || !$node->children['args']->children[0] instanceof Node || $node->children['args']->children[0]->kind !== \ast\AST_VAR || !$node->children['expr'] instanceof Node || empty($node->children['expr']->children['name'] ?? null) || !is_string($node->children['expr']->children['name'])) {
         return $this->context;
     }
     // Translate the function name into the UnionType it asserts
     $map = array('is_array' => 'array', 'is_bool' => 'bool', 'is_callable' => 'callable', 'is_double' => 'float', 'is_float' => 'float', 'is_int' => 'int', 'is_integer' => 'int', 'is_long' => 'int', 'is_null' => 'null', 'is_numeric' => 'string|int|float', 'is_object' => 'object', 'is_real' => 'float', 'is_resource' => 'resource', 'is_scalar' => 'int|float|bool|string|null', 'is_string' => 'string', 'empty' => 'null');
     $functionName = $node->children['expr']->children['name'];
     if (!isset($map[$functionName])) {
         return $this->context;
     }
     $type = UnionType::fromFullyQualifiedString($map[$functionName]);
     $context = $this->context;
     try {
         // Get the variable we're operating on
         $variable = (new ContextNode($this->code_base, $this->context, $node->children['args']->children[0]))->getVariable();
         if ($variable->getUnionType()->isEmpty()) {
             $variable->getUnionType()->addType(NullType::instance());
         }
         // Make a copy of the variable
         $variable = clone $variable;
         $variable->setUnionType(clone $variable->getUnionType());
         // Change the type to match the is_a relationship
         if ($type->isType(ArrayType::instance()) && $variable->getUnionType()->hasGenericArray()) {
             // If the variable is already a generic array,
             // note that it can be an arbitrary array without
             // erasing the existing generic type.
             $variable->getUnionType()->addUnionType($type);
         } else {
             // Otherwise, overwrite the type for any simple
             // primitive types.
             $variable->setUnionType($type);
         }
         // Overwrite the variable with its new type in this
         // scope without overwriting other scopes
         $context = $context->withScopeVariable($variable);
     } catch (\Exception $exception) {
         // Swallow it
     }
     return $context;
 }
Esempio n. 10
0
 /**
  * Takes "a|b[]|c|d[]|e" and returns "b|d"
  *
  * @return UnionType
  * The subset of types in this
  */
 public function genericArrayElementTypes() : UnionType
 {
     // If array is in there, then it can be any type
     // Same for mixed
     if ($this->hasType(ArrayType::instance()) || $this->hasType(MixedType::instance())) {
         return MixedType::instance()->asUnionType();
     }
     if ($this->hasType(ArrayType::instance())) {
         return NullType::instance()->asUnionType();
     }
     return new UnionType(array_filter(array_map(function (Type $type) {
         if (!$type->isGenericArray()) {
             return null;
         }
         return $type->genericArrayElementType();
     }, $this->getTypeList())));
 }
Esempio n. 11
0
 /**
  * Visit a node with kind `\ast\AST_FUNC_DECL`
  *
  * @param Node $node
  * A node to parse
  *
  * @return Context
  * A new or an unchanged context resulting from
  * parsing the node
  */
 public function visitFuncDecl(Decl $node) : Context
 {
     $method = $this->context->getFunctionLikeInScope($this->code_base);
     $return_type = $method->getUnionType();
     if (!$return_type->isEmpty() && !$method->getHasReturn() && !$this->declOnlyThrows($node) && !$return_type->hasType(VoidType::instance()) && !$return_type->hasType(NullType::instance())) {
         $this->emitIssue(Issue::TypeMissingReturn, $node->lineno ?? 0, (string) $method->getFQSEN(), (string) $return_type);
     }
     $parameters_seen = [];
     foreach ($method->getParameterList() as $i => $parameter) {
         if (isset($parameters_seen[$parameter->getName()])) {
             $this->emitIssue(Issue::ParamRedefined, $node->lineno ?? 0, '$' . $parameter->getName());
         } else {
             $parameters_seen[$parameter->getName()] = $i;
         }
     }
     return $this->context;
 }
Esempio n. 12
0
 /**
  * @param Node $node
  * A node to parse
  *
  * @return Context
  * A new or an unchanged context resulting from
  * parsing the node
  */
 public function visitMethod(Decl $node) : Context
 {
     $method = $this->context->getMethodInScope($this->code_base);
     $return_type = $method->getUnionType();
     $has_interface_class = false;
     if ($method->getFQSEN() instanceof FullyQualifiedMethodName) {
         try {
             $class = $method->getDefiningClass($this->code_base);
             $has_interface_class = $class->isInterface();
         } catch (\Exception $exception) {
         }
     }
     if (!$method->isAbstract() && !$has_interface_class && !$return_type->isEmpty() && !$method->getHasReturn() && !$return_type->hasType(VoidType::instance()) && !$return_type->hasType(NullType::instance())) {
         Issue::emit(Issue::TypeMissingReturn, $this->context->getFile(), $node->lineno ?? 0, $method->getFQSEN(), (string) $return_type);
     }
     return $this->context;
 }
Esempio n. 13
0
 /**
  * Visit a node with kind `\ast\AST_FUNC_DECL`
  *
  * @param Node $node
  * A node to parse
  *
  * @return Context
  * A new or an unchanged context resulting from
  * parsing the node
  */
 public function visitFuncDecl(Decl $node) : Context
 {
     $method = $this->context->getMethodInScope($this->code_base);
     $return_type = $method->getUnionType();
     if (!$return_type->isEmpty() && !$method->getHasReturn() && !$return_type->hasType(VoidType::instance()) && !$return_type->hasType(NullType::instance())) {
         $this->emitIssue(Issue::TypeMissingReturn, $node->lineno ?? 0, $method->getFQSEN(), (string) $return_type);
     }
     return $this->context;
 }
Esempio n. 14
0
 /**
  * Takes "a|b[]|c|d[]|e" and returns "b|d"
  *
  * @return UnionType
  * The subset of types in this
  */
 public function genericArrayElementTypes() : UnionType
 {
     // If array is in there, then it can be any type
     // Same for mixed
     if ($this->hasType(ArrayType::instance()) || $this->hasType(MixedType::instance())) {
         return MixedType::instance()->asUnionType();
     }
     if ($this->hasType(ArrayType::instance())) {
         return NullType::instance()->asUnionType();
     }
     return new UnionType($this->type_set->filter(function (Type $type) : bool {
         return $type->isGenericArray();
     })->map(function (Type $type) : Type {
         return $type->genericArrayElementType();
     }));
 }
Esempio n. 15
0
 /**
  * @param Node|string $method_name
  * Either then name of the method or a node that
  * produces the name of the method.
  *
  * @param bool $is_static
  * Set to true if this is a static method call
  *
  * @return Method
  * A method with the given name on the class referenced
  * from the given node
  *
  * @throws NodeException
  * An exception is thrown if we can't understand the node
  *
  * @throws CodeBaseExtension
  * An exception is thrown if we can't find the given
  * method
  *
  * @throws TypeException
  * An exception may be thrown if the only viable candidate
  * is a non-class type.
  *
  * @throws IssueException
  */
 public function getMethod($method_name, bool $is_static) : Method
 {
     if ($method_name instanceof Node) {
         // The method_name turned out to be a variable.
         // There isn't much we can do to figure out what
         // it's referring to.
         throw new NodeException($method_name, "Unexpected method node");
     }
     assert(is_string($method_name), "Method name must be a string. Found non-string at {$this->context}");
     try {
         $class_list = (new ContextNode($this->code_base, $this->context, $this->node->children['expr'] ?? $this->node->children['class']))->getClassList();
     } catch (CodeBaseException $exception) {
         throw new IssueException(Issue::fromType(Issue::UndeclaredClassMethod)($this->context->getFile(), $this->node->lineno ?? 0, [$method_name, (string) $exception->getFQSEN()]));
     }
     // If there were no classes on the left-type, figure
     // out what we were trying to call the method on
     // and send out an error.
     if (empty($class_list)) {
         $union_type = UnionTypeVisitor::unionTypeFromClassNode($this->code_base, $this->context, $this->node->children['expr'] ?? $this->node->children['class']);
         if (!$union_type->isEmpty() && $union_type->isNativeType() && !$union_type->hasAnyType([MixedType::instance(), ObjectType::instance(), StringType::instance()]) && !(Config::get()->null_casts_as_any_type && $union_type->hasType(NullType::instance()))) {
             throw new IssueException(Issue::fromType(Issue::NonClassMethodCall)($this->context->getFile(), $this->node->lineno ?? 0, [$method_name, (string) $union_type]));
         }
         throw new NodeException($this->node, "Can't figure out method call for {$method_name}");
     }
     // Hunt to see if any of them have the method we're
     // looking for
     foreach ($class_list as $i => $class) {
         if ($class->hasMethodWithName($this->code_base, $method_name)) {
             return $class->getMethodByNameInContext($this->code_base, $method_name, $this->context);
         } else {
             if ($class->hasMethodWithName($this->code_base, '__call')) {
                 return $class->getMethodByNameInContext($this->code_base, '__call', $this->context);
             }
         }
     }
     // Figure out an FQSEN for the method we couldn't find
     $method_fqsen = FullyQualifiedMethodName::make($class_list[0]->getFQSEN(), $method_name);
     if ($is_static) {
         throw new IssueException(Issue::fromType(Issue::UndeclaredStaticMethod)($this->context->getFile(), $this->node->lineno ?? 0, [(string) $method_fqsen]));
     }
     throw new IssueException(Issue::fromType(Issue::UndeclaredMethod)($this->context->getFile(), $this->node->lineno ?? 0, [(string) $method_fqsen]));
 }
Esempio n. 16
0
 /**
  * @param Node $node
  * A node to parse
  *
  * @return Context
  * A new or an unchanged context resulting from
  * parsing the node
  */
 public function visitIf(Node $node) : Context
 {
     // Get the list of scopes for each branch of the
     // conditional
     $scope_list = array_map(function (Context $context) {
         return $context->getScope();
     }, $this->child_context_list);
     $has_else = array_reduce($node->children ?? [], function (bool $carry, $child_node) {
         return $carry || $child_node instanceof Node && empty($child_node->children['cond']);
     }, false);
     // If we're not guaranteed to hit at least one
     // branch, mark the incoming scope as a possibility
     if (!$has_else) {
         $scope_list[] = $this->context->getScope();
     }
     // If there weren't multiple branches, continue on
     // as if the conditional never happened
     if (count($scope_list) < 2) {
         return array_values($this->child_context_list)[0];
     }
     // Get a list of all variables in all scopes
     $variable_map = [];
     foreach ($scope_list as $i => $scope) {
         foreach ($scope->getVariableMap() as $name => $variable) {
             $variable_map[$name] = $variable;
         }
     }
     // A function that determins if a variable is defined on
     // every branch
     $is_defined_on_all_branches = function (string $variable_name) use($scope_list) {
         return array_reduce($scope_list, function (bool $has_variable, Scope $scope) use($variable_name) {
             return $has_variable && $scope->hasVariableWithName($variable_name);
         }, true);
     };
     // Get the intersection of all types for all versions of
     // the variable from every side of the branch
     $union_type = function (string $variable_name) use($scope_list) {
         // Get a list of all variables with the given name from
         // each scope
         $variable_list = array_filter(array_map(function (Scope $scope) use($variable_name) {
             if (!$scope->hasVariableWithName($variable_name)) {
                 return null;
             }
             return $scope->getVariableByName($variable_name);
         }, $scope_list));
         // Get the list of types for each version of the variable
         $type_set_list = array_map(function (Variable $variable) : Set {
             return $variable->getUnionType()->getTypeSet();
         }, $variable_list);
         if (count($type_set_list) < 2) {
             return new UnionType($type_set_list[0] ?? []);
         }
         return new UnionType(Set::unionAll($type_set_list));
     };
     // Clone the incoming scope so we can modify it
     // with the outgoing merged scope
     $scope = clone $this->context->getScope();
     foreach ($variable_map as $name => $variable) {
         // Skip variables that are only partially defined
         if (!$is_defined_on_all_branches($name)) {
             if ($this->context->getIsStrictTypes()) {
                 continue;
             } else {
                 $variable->getUnionType()->addType(NullType::instance());
             }
         }
         // Limit the type of the variable to the subset
         // of types that are common to all branches
         $variable = clone $variable;
         $variable->setUnionType($union_type($name));
         // Add the variable to the outgoing scope
         $scope->addVariable($variable);
     }
     // Set the new scope with only the variables and types
     // that are common to all branches
     return $this->context->withScope($scope);
 }
Esempio n. 17
0
 /**
  * Visit a node with kind `\ast\AST_DIM`
  *
  * @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 UnionType
  * The set of types that are possibly produced by the
  * given node
  */
 public function visitDim(Node $node) : UnionType
 {
     $union_type = self::unionTypeFromNode($this->code_base, $this->context, $node->children['expr']);
     if ($union_type->isEmpty()) {
         return $union_type;
     }
     // Figure out what the types of accessed array
     // elements would be
     $generic_types = $union_type->genericArrayElementTypes();
     // If we have generics, we're all set
     if (!$generic_types->isEmpty()) {
         return $generic_types;
     }
     // If the only type is null, we don't know what
     // accessed items will be
     if ($union_type->isType(NullType::instance())) {
         return new UnionType();
     }
     $element_types = new UnionType();
     // You can access string characters via array index,
     // so we'll add the string type to the result if we're
     // indexing something that could be a string
     if ($union_type->isType(StringType::instance()) || $union_type->canCastToUnionType(StringType::instance()->asUnionType())) {
         $element_types->addType(StringType::instance());
     }
     // array offsets work on strings, unfortunately
     // Double check that any classes in the type don't
     // have ArrayAccess
     $array_access_type = Type::fromNamespaceAndName('\\', 'ArrayAccess');
     // Hunt for any types that are viable class names and
     // see if they inherit from ArrayAccess
     try {
         foreach ($union_type->asClassList($this->code_base) as $class) {
             if ($class->getUnionType()->hasType($array_access_type)) {
                 return $element_types;
             }
         }
     } catch (CodeBaseException $exception) {
         // Swallow it
     }
     if ($element_types->isEmpty()) {
         $this->emitIssue(Issue::TypeArraySuspicious, $node->lineno ?? 0, (string) $union_type);
     }
     return $element_types;
 }
 /**
  * @param Node $node
  * A node to parse
  *
  * @return Context
  * A new or an unchanged context resulting from
  * parsing the node
  */
 public function visitMethod(Decl $node) : Context
 {
     $method = $this->context->getMethodInScope($this->code_base);
     $return_type = $method->getUnionType();
     $has_interface_class = false;
     if ($method->getFQSEN() instanceof FullyQualifiedMethodName) {
         try {
             $class = $method->getDefiningClass($this->code_base);
             $has_interface_class = $class->isInterface();
         } catch (\Exception $exception) {
         }
     }
     if (!$method->isAbstract() && !$has_interface_class && !$return_type->isEmpty() && !$method->getHasReturn() && !$return_type->hasType(VoidType::instance()) && !$return_type->hasType(NullType::instance())) {
         Log::err(Log::ETYPE, "Method {$method->getFQSEN()} is declared to return {$return_type} but has no return value", $this->context->getFile(), $node->lineno);
     }
     return $this->context;
 }
Esempio n. 19
0
 /**
  * @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;
 }
Esempio n. 20
0
 /**
  * @return Parameter
  * A parameter built from a node
  *
  * @see \Phan\Deprecated\Pass1::node_param
  * Formerly `function node_param`
  */
 public static function fromNode(Context $context, CodeBase $code_base, Node $node) : Parameter
 {
     assert($node instanceof Node, "node was not an \\ast\\Node");
     // Get the type of the parameter
     $type = UnionType::fromSimpleNode($context, $node->children['type']);
     $comment = Comment::fromStringInContext($node->docComment ?? '', $context);
     // Create the skeleton parameter from what we know so far
     $parameter = new Parameter($context, (string) $node->children['name'], $type, $node->flags ?? 0);
     // If there is a default value, store it and its type
     if (($default_node = $node->children['default']) !== null) {
         // We can't figure out default values during the
         // parsing phase, unfortunately
         if (!$default_node instanceof Node || $default_node->kind == \ast\AST_CONST || $default_node->kind == \ast\AST_UNARY_OP || $default_node->kind == \ast\AST_ARRAY) {
             // Set the default value
             $parameter->setDefaultValue($node->children['default'], UnionType::fromNode($context, $code_base, $node->children['default']));
         } else {
             // Nodes here may be of type \ast\AST_CLASS_CONST
             // which we can't figure out during the first
             // parsing pass
             $parameter->setDefaultValue(null, NullType::instance()->asUnionType());
         }
     }
     return $parameter;
 }
Esempio n. 21
0
 /**
  * @param string $string
  * A string representing a type
  *
  * @param Context $context
  * The context in which the type string was
  * found
  *
  * @return Type
  * Parse a type from the given string
  */
 public static function fromStringInContext(string $string, Context $context) : Type
 {
     assert($string !== '', "Type cannot be empty in {$context}");
     $namespace = null;
     // Extract the namespace if the type string is
     // fully-qualified
     if ('\\' === $string[0]) {
         list($namespace, $string) = self::namespaceAndTypeFromString($string);
     }
     $type_name = $string;
     // @var bool
     // True if this type name if of the form 'C[]'
     $is_generic_array_type = self::isGenericArrayString($type_name);
     // If this is a generic array type, get the name of
     // the type of each element
     $non_generic_array_type_name = $type_name;
     if ($is_generic_array_type && false !== ($pos = strpos($type_name, '[]'))) {
         $non_generic_array_type_name = substr($type_name, 0, $pos);
     }
     // Check to see if the type name is mapped via
     // a using clause.
     //
     // Gotta check this before checking for native types
     // because there are monsters out there that will
     // remap the names via things like `use \Foo\String`.
     if ($context->hasNamespaceMapFor(T_CLASS, $non_generic_array_type_name)) {
         $fqsen = $context->getNamespaceMapFor(T_CLASS, $non_generic_array_type_name);
         if ($is_generic_array_type) {
             return GenericArrayType::fromElementType(Type::make($fqsen->getNamespace(), $fqsen->getName()));
         }
         return Type::make($fqsen->getNamespace(), $fqsen->getName());
     }
     // If this was a fully qualified type, we're all
     // set
     if (!empty($namespace)) {
         return self::fromNamespaceAndName($namespace, $type_name);
     }
     if ($is_generic_array_type && self::isNativeTypeString($type_name)) {
         return self::fromInternalTypeName($type_name);
     } else {
         // Check to see if its a builtin type
         switch (self::canonicalNameFromName($type_name)) {
             case 'array':
                 return \Phan\Language\Type\ArrayType::instance();
             case 'bool':
                 return \Phan\Language\Type\BoolType::instance();
             case 'callable':
                 return \Phan\Language\Type\CallableType::instance();
             case 'float':
                 return \Phan\Language\Type\FloatType::instance();
             case 'int':
                 return \Phan\Language\Type\IntType::instance();
             case 'mixed':
                 return \Phan\Language\Type\MixedType::instance();
             case 'null':
                 return \Phan\Language\Type\NullType::instance();
             case 'object':
                 return \Phan\Language\Type\ObjectType::instance();
             case 'resource':
                 return \Phan\Language\Type\ResourceType::instance();
             case 'string':
                 return \Phan\Language\Type\StringType::instance();
             case 'void':
                 return \Phan\Language\Type\VoidType::instance();
         }
     }
     // Things like `self[]` or `$this[]`
     if ($is_generic_array_type && self::isSelfTypeString($non_generic_array_type_name) && $context->isInClassScope()) {
         // Callers of this method should be checking on their own
         // to see if this type is a reference to 'parent' and
         // dealing with it there. We don't want to have this
         // method be dependent on the code base
         assert('parent' !== $non_generic_array_type_name, __METHOD__ . " does not know how to handle the type name 'parent' in {$context}");
         return GenericArrayType::fromElementType(static::fromFullyQualifiedString((string) $context->getClassFQSEN()));
     }
     // If this is a type referencing the current class
     // in scope such as 'self' or 'static', return that.
     if (self::isSelfTypeString($type_name) && $context->isInClassScope()) {
         // Callers of this method should be checking on their own
         // to see if this type is a reference to 'parent' and
         // dealing with it there. We don't want to have this
         // method be dependent on the code base
         assert('parent' !== $type_name, __METHOD__ . " does not know how to handle the type name 'parent' in {$context}");
         return static::fromFullyQualifiedString((string) $context->getClassFQSEN());
     }
     // Attach the context's namespace to the type name
     return self::fromNamespaceAndName($context->getNamespace() ?: '\\', $type_name);
 }
Esempio n. 22
0
 /**
  * @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
  *
  *
  * @see \Phan\Deprecated\Pass1::node_func
  * Formerly 'function node_func'
  */
 public static function fromNode(Context $context, CodeBase $code_base, Node $node) : Method
 {
     // 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) {
         $context = $context->withScopeVariable($parameter);
     }
     // Create the skeleton method object from what
     // we know so far
     $method = new Method($context, $node->name, new UnionType(), $node->flags ?? 0);
     // 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());
     // Take a look at method return types
     if ($node->children['returnType'] !== null) {
         $union_type = UnionType::fromSimpleNode($context, $node->children['returnType']);
         $method->getUnionType()->addUnionType($union_type);
     } else {
         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->hasClassFQSEN()) {
                     $union_type = $union_type->addUnionType($context->getClassFQSEN()->asUnionType());
                 }
             }
             $method->getUnionType()->addUnionType($union_type);
         }
     }
     // Add params to local scope for user functions
     if ($context->getFile() != 'internal') {
         $parameter_offset = 0;
         foreach ($method->parameter_list 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->hasParameterAtOffset($parameter_offset)) {
                     $comment_type = $comment->getParameterAtOffset($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->canCastToUnionType($parameter->getUnionType())) {
                     Log::err(Log::ETYPE, "Default value for {$parameter->getUnionType()} \${$parameter->getName()} can't be {$default_type}", $context->getFile(), $node->lineno);
                 }
                 // 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;
 }