/** * @param CodeBase * The code base to use in order to find super classes, etc. * * @param $recursion_depth * This thing has a tendency to run-away on me. This tracks * how bad I messed up by seeing how far the expanded types * go * * @return UnionType * Expands class types to all inherited classes returning * a superset of this type. */ public function asExpandedTypes(CodeBase $code_base, int $recursion_depth = 0) : UnionType { return $this->memoize(__METHOD__, function () use($code_base, $recursion_depth) : UnionType { // We're going to assume that if the type hierarchy // is taller than some value we probably messed up // and should bail out. assert($recursion_depth < 20, "Recursion has gotten out of hand for type {$this}"); if ($this->isNativeType()) { return $this->asUnionType(); } $union_type = $this->asUnionType(); $class_fqsen = $this->isGenericArray() ? $this->genericArrayElementType()->asFQSEN() : $this->asFQSEN(); if (!$code_base->hasClassWithFQSEN($class_fqsen)) { return $union_type; } $clazz = $code_base->getClassByFQSEN($class_fqsen); $union_type->addUnionType($this->isGenericArray() ? $clazz->getUnionType()->asGenericArrayTypes() : $clazz->getUnionType()); // Resurse up the tree to include all types $recursive_union_type = new UnionType(); foreach ($union_type->getTypeSet() as $clazz_type) { if ((string) $clazz_type != (string) $this) { $recursive_union_type->addUnionType($clazz_type->asExpandedTypes($code_base, $recursion_depth + 1)); } else { $recursive_union_type->addType($clazz_type); } } return $recursive_union_type; }); }
/** * 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; }
/** * @param CodeBase * The code base to use in order to find super classes, etc. * * @param $recursion_depth * This thing has a tendency to run-away on me. This tracks * how bad I messed up by seeing how far the expanded types * go * * @return UnionType * Expands class types to all inherited classes returning * a superset of this type. */ public function asExpandedTypes(CodeBase $code_base, int $recursion_depth = 0) : UnionType { return $this->memoize(__METHOD__, function () use($code_base, $recursion_depth) : UnionType { assert($recursion_depth < 10, "Recursion has gotten out of hand for type {$this}"); if ($this->isNativeType()) { return $this->asUnionType(); } $union_type = $this->asUnionType(); $class_fqsen = $this->isGenericArray() ? $this->genericArrayElementType()->asFQSEN() : $this->asFQSEN(); if (!$code_base->hasClassWithFQSEN($class_fqsen)) { return $union_type; } $clazz = $code_base->getClassByFQSEN($class_fqsen); $union_type->addUnionType($this->isGenericArray() ? $clazz->getUnionType()->asGenericArrayTypes() : $clazz->getUnionType()); // Resurse up the tree to include all types $recursive_union_type = new UnionType(); foreach ($union_type->getTypeList() as $clazz_type) { if ((string) $clazz_type != (string) $this) { $recursive_union_type->addUnionType($clazz_type->asExpandedTypes($code_base, $recursion_depth + 1)); } else { $recursive_union_type->addType($clazz_type); } } return $recursive_union_type; }); }
/** * 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; }
/** * Takes "a|b[]|c|d[]|e" and returns "b|d" * * @return UnionType * The subset of types in this */ public function genericArrayElementTypes() : UnionType { $union_type = new UnionType($this->type_set->filter(function (Type $type) : bool { return $type->isGenericArray(); })->map(function (Type $type) : Type { return $type->genericArrayElementType(); })); // If array is in there, then it can be any type // Same for mixed if ($this->hasType(ArrayType::instance()) || $this->hasType(MixedType::instance())) { $union_type->addType(MixedType::instance()); } if ($this->hasType(ArrayType::instance())) { $union_type->addType(NullType::instance()); } return $union_type; }