/** * @param Context $context * @param CodeBase $code_base * @param Node|string|null $node * * @return UnionType * * @see \Phan\Deprecated\Pass2::node_type * Formerly 'function node_type' */ public static function fromNode(Context $context, CodeBase $code_base, $node) : UnionType { if (!$node instanceof Node) { if ($node === null) { return new UnionType(); } return Type::fromObject($node)->asUnionType(); } return (new Element($node))->acceptKindVisitor(new UnionTypeVisitor($context, $code_base)); }
private function visitClassNode(Node $node) : UnionType { // Things of the form `new $class_name();` if ($node->kind == \ast\AST_VAR) { return new UnionType(); } // Anonymous class of form `new class { ... }` if ($node->kind == \ast\AST_CLASS && $node->flags & \ast\flags\CLASS_ANONYMOUS) { // Generate a stable name for the anonymous class $anonymous_class_name = (new ContextNode($this->code_base, $this->context, $node))->getUnqualifiedNameForAnonymousClass(); // Turn that into a fully qualified name $fqsen = FullyQualifiedClassName::fromStringInContext($anonymous_class_name, $this->context); // Turn that into a union type return Type::fromFullyQualifiedString((string) $fqsen)->asUnionType(); } // Things of the form `new $method->name()` if ($node->kind !== \ast\AST_NAME) { return new UnionType(); } // Get the name of the class $class_name = $node->children['name']; // If this is a straight-forward class name, recurse into the // class node and get its type if (!Type::isSelfTypeString($class_name)) { // TODO: does anyone else call this method? return self::unionTypeFromClassNode($this->code_base, $this->context, $node); } // This is a self-referential node if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, "Cannot access {$class_name} when not in a class scope", $this->context->getFile(), $node->lineno); return new UnionType(); } // Reference to a parent class if ($class_name === 'parent') { $class = $this->context->getClassInScope($this->code_base); if (!$class->hasParentClassFQSEN()) { Log::err(Log::ESTATIC, "Reference to parent of parentless class {$class->getFQSEN()}", $this->context->getFile(), $node->lineno); return new UnionType(); } return Type::fromFullyQualifiedString((string) $class->getParentClassFQSEN())->asUnionType(); } return Type::fromFullyQualifiedString((string) $this->context->getClassFQSEN())->asUnionType(); }
/** * @param Context $context * The context in which the comment line appears * * @param string $line * An individual line of a comment * * @return Option<Type> * An optional type overriding the extended type of the class */ private static function inheritsFromCommentLine(Context $context, string $line) { $match = []; if (preg_match('/@inherits\\s+(' . Type::type_regex . ')/', $line, $match)) { $type_string = $match[1]; $type = new Some(Type::fromStringInContext($type_string, $context)); return $type; } return new None(); }
/** * @return bool * True if this Type can be cast to the given Type * cleanly */ public function canCastToType(Type $type) : bool { if ($this === $type) { return true; } $s = strtolower((string) $this); $d = strtolower((string) $type); $s_is_generic_array = $this->isGenericArray(); $d_is_generic_array = $type->isGenericArray(); if ($s[0] == '\\') { $s = substr($s, 1); } if ($d[0] == '\\') { $d = substr($d, 1); } if ($s === $d) { return true; } if (Config::get()->scalar_implicit_cast) { if ($type->isScalar() && $this->isScalar()) { return true; } } if ($s_is_generic_array && $d_is_generic_array) { return $this->genericArrayElementType()->canCastToType($type->genericArrayElementType()); } if ($s === 'int' && $d === 'float') { return true; // int->float is ok } if (($s === 'array' || $s === 'string' || $s_is_generic_array || $s === 'closure') && $d === 'callable') { return true; } if ($s === 'object' && !$type->isScalar() && $d !== 'array') { return true; } if ($d === 'object' && !$this->isScalar() && $s !== 'array') { return true; } if ($s_is_generic_array && ($d == 'array' || $d == 'arrayaccess')) { return true; } if ($d_is_generic_array && $s === 'array') { return true; } if ($s === 'callable' && $d === 'closure') { return true; } if (($pos = strrpos($d, '\\')) !== false) { if ('\\' !== $this->getNamespace()) { if (trim($this->getNamespace() . '\\' . $s, '\\') == $d) { return true; } } else { if (substr($d, $pos + 1) === $s) { return true; // Lazy hack, but... } } } if (($pos = strrpos($s, '\\')) !== false) { if ('\\' !== $type->getNamespace()) { if (trim($type->getNamespace() . '\\' . $d, '\\') == $s) { return true; } } else { if (substr($s, $pos + 1) === $d) { return true; // Lazy hack, but... } } } return false; }
/** * Visit a node with kind `\ast\AST_UNARY_MINUS` * * @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 visitUnaryMinus(Node $node) : UnionType { return Type::fromObject($node->children['expr'])->asUnionType(); }
/** * @return Type * The type of this class */ public function asType() : Type { return Type::fromFullyQualifiedString((string) $this); }
/** * @param Type|null $parent_type * The type of the parent (extended) class of this class. * * @return void */ public function setParentType(Type $parent_type = null) { if ($this->getInternalScope()->hasAnyTemplateType()) { // Get a reference to the local list of templated // types. We'll use this to map templated types on the // parent to locally templated types. $template_type_map = $this->getInternalScope()->getTemplateTypeMap(); // Figure out if the given parent type contains any template // types. $contains_templated_type = false; foreach ($parent_type->getTemplateParameterTypeList() as $i => $union_type) { foreach ($union_type->getTypeSet() as $type) { if (isset($template_type_map[$type->getName()])) { $contains_templated_type = true; break 2; } } } // If necessary, map the template parameter type list through the // local list of templated types. if ($contains_templated_type) { $parent_type = Type::fromType($parent_type, array_map(function (UnionType $union_type) use($template_type_map) : UnionType { return new UnionType(array_map(function (Type $type) use($template_type_map) : Type { return $template_type_map[$type->getName()] ?? $type; }, $union_type->getTypeSet()->toArray())); }, $parent_type->getTemplateParameterTypeList())); } } $this->parent_type = $parent_type; // Add the parent to the union type of this // class $this->getUnionType()->addUnionType($parent_type->asUnionType()); }
/** * As per the Serializable interface * * @param string $serialized * A serialized UnionType * * @return void * * @see \Serializable */ public function unserialize($serialized) { $this->type_set = new Set(array_map(function (string $type_name) { return Type::fromFullyQualifiedString($type_name); }, explode('|', $serialized ?? ''))); }
/** * @param CodeBase $code_base * The code base within which we're operating * * @param $context $context * The context of the parser at the node for which we'd * like to determine a type * * @param Node|mixed $node * The node for which we'd like to determine its type * * @return UnionType * The UnionType associated with the given node * in the given Context within the given CodeBase * * @throws IssueException * An exception is thrown if we can't find a class for * the given type */ public static function unionTypeFromClassNode(CodeBase $code_base, Context $context, $node) : UnionType { // For simple nodes or very complicated nodes, // recurse if (!$node instanceof \ast\Node || $node->kind != \ast\AST_NAME) { return self::unionTypeFromNode($code_base, $context, $node); } $class_name = $node->children['name']; if ('parent' === $class_name) { if (!$context->isInClassScope()) { throw new IssueException(Issue::fromType(Issue::ContextNotObject)($context->getFile(), $node->lineno ?? 0, [$class_name])); } $class = $context->getClassInScope($code_base); if ($class->isTrait()) { throw new IssueException(Issue::fromType(Issue::TraitParentReference)($context->getFile(), $node->lineno ?? 0, [(string) $context->getClassFQSEN()])); } if (!$class->hasParentType()) { throw new IssueException(Issue::fromType(Issue::ParentlessClass)($context->getFile(), $node->lineno ?? 0, [(string) $context->getClassFQSEN()])); } $parent_class_fqsen = $class->getParentClassFQSEN(); if (!$code_base->hasClassWithFQSEN($parent_class_fqsen)) { throw new IssueException(Issue::fromType(Issue::UndeclaredClass)($context->getFile(), $node->lineno ?? 0, [(string) $parent_class_fqsen])); } else { $parent_class = $code_base->getClassByFQSEN($parent_class_fqsen); return $parent_class->getUnionType(); } } // We're going to convert the class reference to a type $type = null; // Check to see if the name is fully qualified if (!($node->flags & \ast\flags\NAME_NOT_FQ)) { if (0 !== strpos($class_name, '\\')) { $class_name = '\\' . $class_name; } $type = Type::fromFullyQualifiedString($class_name); } else { $type = Type::fromStringInContext($class_name, $context); } return $type->asUnionType(); }
/** * @param string $type_string * A '|' delimited string representing a type in the form * 'int|string|null|ClassName'. * * @param Context $context * The context in which the type string was * found * * @return UnionType */ public static function fromStringInContext(string $type_string, Context $context) : UnionType { if (empty($type_string)) { return new UnionType(); } return new UnionType(array_map(function (string $type_name) use($context, $type_string) { assert($type_name !== '', "Type cannot be empty. Type '{$type_name}' given as part of the union type '{$type_string}' in {$context}."); return Type::fromStringInContext($type_name, $context); }, array_filter(array_map(function (string $type_name) { return trim($type_name); }, explode('|', $type_string))))); }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitReturn(Node $node) : Context { // Don't check return types in traits if ($this->context->isInClassScope()) { $clazz = $this->context->getClassInScope($this->code_base); if ($clazz->isTrait()) { return $this->context; } } // Make sure we're actually returning from a method. if (!$this->context->isInFunctionLikeScope()) { return $this->context; } // Get the method/function/closure we're in $method = $this->context->getFunctionLikeInScope($this->code_base); assert(!empty($method), "We're supposed to be in either method or closure scope."); // Figure out what we intend to return $method_return_type = $method->getUnionType(); // Figure out what is actually being returned $expression_type = UnionType::fromNode($this->context, $this->code_base, $node->children['expr']); if ($expression_type->hasStaticType()) { $expression_type = $expression_type->withStaticResolvedInContext($this->context); } // If there is no declared type, see if we can deduce // what it should be based on the return type if ($method_return_type->isEmpty() || $method->isReturnTypeUndefined()) { $method->setIsReturnTypeUndefined(true); // Set the inferred type of the method based // on what we're returning $method->getUnionType()->addUnionType($expression_type); // No point in comparing this type to the // type we just set return $this->context; } if (!$method->isReturnTypeUndefined() && !$expression_type->canCastToExpandedUnionType($method_return_type, $this->code_base) && !$method->getUnionType()->canCastToExpandedUnionType(Type::fromNamespaceAndName('\\', 'Generator')->asUnionType(), $this->code_base)) { $this->emitIssue(Issue::TypeMismatchReturn, $node->lineno ?? 0, (string) $expression_type, $method->getName(), (string) $method_return_type); } if ($method->isReturnTypeUndefined()) { // Add the new type to the set of values returned by the // method $method->getUnionType()->addUnionType($expression_type); } // Mark the method as returning something $method->setHasReturn(($node->children['expr'] ?? null) !== null); return $this->context; }
public function testGenericArrayTypeFromString() { $type = Type::fromFullyQualifiedString("int[][]"); $this->assertEquals($type->genericArrayElementType()->__toString(), "int[]"); }
/** * @param CodeBase $code_base * The global code base * * @param FunctionInterface $method * The method we're analyzing arguments for * * @param Node $node * The node holding the method call we're looking at * * @param Context $context * The context in which we see the call * * @return null * * @see \Phan\Deprecated\Pass2::arglist_type_check * Formerly `function arglist_type_check` */ private static function analyzeParameterList(CodeBase $code_base, FunctionInterface $method, Node $node, Context $context) { foreach ($node->children ?? [] as $i => $argument) { // Get the parameter associated with this argument $parameter = $method->getParameterList()[$i] ?? null; // This issue should be caught elsewhere if (!$parameter) { continue; } // If this is a pass-by-reference parameter, make sure // we're passing an allowable argument if ($parameter->isPassByReference()) { if (!$argument instanceof \ast\Node || $argument->kind != \ast\AST_VAR && $argument->kind != \ast\AST_DIM && $argument->kind != \ast\AST_PROP && $argument->kind != \ast\AST_STATIC_PROP) { Issue::maybeEmit($code_base, $context, Issue::TypeNonVarPassByRef, $node->lineno ?? 0, $i + 1, (string) $method->getFQSEN()); } else { $variable_name = (new ContextNode($code_base, $context, $argument))->getVariableName(); if (Type::isSelfTypeString($variable_name) && !$context->isInClassScope() && $argument->kind == \ast\AST_STATIC_PROP && $argument->kind == \ast\AST_PROP) { Issue::maybeEmit($code_base, $context, Issue::ContextNotObject, $node->lineno ?? 0, "{$variable_name}"); } } } // Get the type of the argument. We'll check it against // the parameter in a moment $argument_type = UnionType::fromNode($context, $code_base, $argument); // Expand it to include all parent types up the chain $argument_type_expanded = $argument_type->asExpandedTypes($code_base); // Check the method to see if it has the correct // parameter types. If not, keep hunting through // alternates of the method until we find one that // takes the correct types $alternate_parameter = null; $alternate_found = false; foreach ($method->alternateGenerator($code_base) as $alternate_id => $alternate_method) { if (empty($alternate_method->getParameterList()[$i])) { continue; } // Get the parameter associated with this argument $alternate_parameter = $alternate_method->getParameterList()[$i] ?? null; // Expand the types to find all parents and traits $alternate_parameter_type_expanded = $alternate_parameter->getUnionType()->asExpandedTypes($code_base); // See if the argument can be cast to the // parameter if ($argument_type_expanded->canCastToUnionType($alternate_parameter_type_expanded)) { $alternate_found = true; break; } } if (!$alternate_found) { $parameter_name = $alternate_parameter ? $alternate_parameter->getName() : 'unknown'; $parameter_type = $alternate_parameter ? $alternate_parameter->getUnionType() : 'unknown'; if ($method->isInternal()) { Issue::maybeEmit($code_base, $context, Issue::TypeMismatchArgumentInternal, $node->lineno ?? 0, $i + 1, $parameter_name, $argument_type_expanded, (string) $method->getFQSEN(), (string) $parameter_type); } else { Issue::maybeEmit($code_base, $context, Issue::TypeMismatchArgument, $node->lineno ?? 0, $i + 1, $parameter_name, $argument_type_expanded, (string) $method->getFQSEN(), (string) $parameter_type, $method->getFileRef()->getFile(), $method->getFileRef()->getLineNumberStart()); } } } }
/** * @param CodeBase $code_base * A reference to the entire code base in which this * context exists * * @param ReflectionClass $class * A reflection class representing a builtin class. * * @return Clazz * A Class structural element representing the given named * builtin. */ public static function fromReflectionClass(CodeBase $code_base, \ReflectionClass $class) : Clazz { // Build a set of flags based on the constitution // of the built-in class $flags = 0; if ($class->isFinal()) { $flags = \ast\flags\CLASS_FINAL; } else { if ($class->isInterface()) { $flags = \ast\flags\CLASS_INTERFACE; } else { if ($class->isTrait()) { $flags = \ast\flags\CLASS_TRAIT; } } } if ($class->isAbstract()) { $flags |= \ast\flags\CLASS_ABSTRACT; } $context = new Context(); // Build a base class element $clazz = new Clazz($context, $class->getName(), UnionType::fromStringInContext($class->getName(), $context), $flags); // If this class has a parent class, add it to the // class info if ($parent_class = $class->getParentClass()) { $parent_class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString('\\' . $parent_class->getName()); $clazz->setParentClassFQSEN($parent_class_fqsen); } foreach ($class->getDefaultProperties() as $name => $value) { // TODO: whats going on here? $reflection_property = new \ReflectionProperty($class->getName(), $name); $property = new Property($context->withClassFQSEN($clazz->getFQSEN()), $name, Type::fromObject($value)->asUnionType(), 0); $clazz->addProperty($code_base, $property); } foreach ($class->getInterfaceNames() as $name) { $clazz->addInterfaceClassFQSEN(FullyQualifiedClassName::fromFullyQualifiedString('\\' . $name)); } foreach ($class->getTraitNames() as $name) { $clazz->addTraitFQSEN(FullyQualifiedClassName::fromFullyQualifiedString('\\' . $name)); } foreach ($class->getConstants() as $name => $value) { $clazz->addConstant($code_base, new Constant($context, $name, Type::fromObject($value)->asUnionType(), 0)); } foreach ($class->getMethods() as $reflection_method) { $method_list = Method::methodListFromReflectionClassAndMethod($context->withClassFQSEN($clazz->getFQSEN()), $code_base, $class, $reflection_method); foreach ($method_list as $method) { $clazz->addMethod($code_base, $method); } } return $clazz; }
/** * The return type of the given FunctionInterface to a Generator. * Emit an Issue if the documented return type is incompatible with that. * @return void */ private function setReturnTypeOfGenerator(FunctionInterface $func, Node $node) { // Currently, there is no way to describe the types passed to // a Generator in phpdoc. // So, nothing bothers recording the types beyond \Generator. $func->setHasReturn(true); // Returns \Generator, technically $func->setHasYield(true); if ($func->getUnionType()->isEmpty()) { $func->setIsReturnTypeUndefined(true); $func->getUnionType()->addUnionType(Type::fromNamespaceAndName('\\', 'Generator')->asUnionType()); } if (!$func->isReturnTypeUndefined()) { $func_return_type = $func->getUnionType(); if (!$func_return_type->canCastToExpandedUnionType(Type::fromNamespaceAndName('\\', 'Generator')->asUnionType(), $this->code_base)) { // At least one of the documented return types must // be Generator, Iterable, or Traversable. // Check for the issue here instead of in visitReturn/visitYield so that // the check is done exactly once. $this->emitIssue(Issue::TypeMismatchReturn, $node->lineno ?? 0, '\\Generator', $func->getName(), (string) $func_return_type); } } }
/** * @return bool * True if this Type can be cast to the given Type * cleanly */ public function canCastToType(Type $type) : bool { if ($this === $type) { return true; } $s = (string) $this; $d = (string) $type; if ($s[0] == '\\') { $s = substr($s, 1); } if ($d[0] == '\\') { $d = substr($d, 1); } if ($s === $d) { return true; } if ($s === 'int' && $d === 'float') { return true; // int->float is ok } if (($s === 'array' || $s === 'string' || strpos($s, '[]') !== false) && $d === 'callable') { return true; } if ($s === 'object' && !$type->isScalar() && $d !== 'array') { return true; } if ($d === 'object' && !$this->isScalar() && $s !== 'array') { return true; } if (strpos($s, '[]') !== false && $d === 'array') { return true; } if (strpos($d, '[]') !== false && $s === 'array') { return true; } if ($s === 'callable' && $d === 'closure') { return true; } if (($pos = strrpos($d, '\\')) !== false) { if ('\\' !== $this->getNamespace()) { if (trim($this->getNamespace() . '\\' . $s, '\\') == $d) { return true; } } else { if (substr($d, $pos + 1) === $s) { return true; // Lazy hack, but... } } } if (($pos = strrpos($s, '\\')) !== false) { if ('\\' !== $type->getNamespace()) { if (trim($type->getNamespace() . '\\' . $d, '\\') == $s) { return true; } } else { if (substr($s, $pos + 1) === $d) { return true; // Lazy hack, but... } } } return false; }
/** * @param CodeBase $code_base * The global code base * * @param FunctionInterface $method * The method we're analyzing arguments for * * @param Node $node * The node holding the method call we're looking at * * @param Context $context * The context in which we see the call * * @return null * * @see \Phan\Deprecated\Pass2::arglist_type_check * Formerly `function arglist_type_check` */ private static function analyzeParameterList(CodeBase $code_base, FunctionInterface $method, Node $node, Context $context) { // There's nothing reasonable we can do here if ($method instanceof Method) { if ($method->getIsMagicCall() || $method->getIsMagicCallStatic()) { return; } } foreach ($node->children ?? [] as $i => $argument) { // Get the parameter associated with this argument $parameter = $method->getParameterForCaller($i); // This issue should be caught elsewhere if (!$parameter) { continue; } // If this is a pass-by-reference parameter, make sure // we're passing an allowable argument if ($parameter->isPassByReference()) { if (!$argument instanceof \ast\Node || $argument->kind != \ast\AST_VAR && $argument->kind != \ast\AST_DIM && $argument->kind != \ast\AST_PROP && $argument->kind != \ast\AST_STATIC_PROP) { Issue::maybeEmit($code_base, $context, Issue::TypeNonVarPassByRef, $node->lineno ?? 0, $i + 1, (string) $method->getFQSEN()); } else { $variable_name = (new ContextNode($code_base, $context, $argument))->getVariableName(); if (Type::isSelfTypeString($variable_name) && !$context->isInClassScope() && $argument->kind == \ast\AST_STATIC_PROP && $argument->kind == \ast\AST_PROP) { Issue::maybeEmit($code_base, $context, Issue::ContextNotObject, $node->lineno ?? 0, "{$variable_name}"); } } } // Get the type of the argument. We'll check it against // the parameter in a moment $argument_type = UnionType::fromNode($context, $code_base, $argument); // Expand it to include all parent types up the chain $argument_type_expanded = $argument_type->asExpandedTypes($code_base); // Check the method to see if it has the correct // parameter types. If not, keep hunting through // alternates of the method until we find one that // takes the correct types $alternate_parameter = null; $alternate_found = false; foreach ($method->alternateGenerator($code_base) as $alternate_id => $alternate_method) { // Get the parameter associated with this argument $candidate_alternate_parameter = $alternate_method->getParameterForCaller($i); if (is_null($candidate_alternate_parameter)) { continue; } $alternate_parameter = $candidate_alternate_parameter; // See if the argument can be cast to the // parameter if ($argument_type_expanded->canCastToUnionType($alternate_parameter->getUnionType())) { $alternate_found = true; break; } } if (!$alternate_found) { $parameter_name = $alternate_parameter ? $alternate_parameter->getName() : 'unknown'; $parameter_type = $alternate_parameter ? $alternate_parameter->getUnionType() : 'unknown'; if (is_object($parameter_type) && $parameter_type->hasTemplateType()) { // Don't worry about template types } elseif ($method->isInternal()) { // If we are not in strict mode and we accept a string parameter // and the argument we are passing has a __toString method then it is ok if (!$context->getIsStrictTypes() && $parameter_type->hasType(StringType::instance())) { try { foreach ($argument_type_expanded->asClassList($code_base, $context) as $clazz) { if ($clazz->hasMethodWithName($code_base, "__toString")) { return; } } } catch (CodeBaseException $e) { // Swallow "Cannot find class", go on to emit issue } } Issue::maybeEmit($code_base, $context, Issue::TypeMismatchArgumentInternal, $node->lineno ?? 0, $i + 1, $parameter_name, $argument_type_expanded, (string) $method->getFQSEN(), (string) $parameter_type); } else { Issue::maybeEmit($code_base, $context, Issue::TypeMismatchArgument, $node->lineno ?? 0, $i + 1, $parameter_name, $argument_type_expanded, (string) $method->getFQSEN(), (string) $parameter_type, $method->getFileRef()->getFile(), $method->getFileRef()->getLineNumberStart()); } } } }