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