1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-04 10:38:49 +01:00
psalm/src/Psalm/TypeChecker.php
2016-08-10 19:21:03 -04:00

857 lines
32 KiB
PHP

<?php
namespace Psalm;
use Psalm\Issue\InvalidArgument;
use Psalm\Issue\FailedTypeResolution;
use Psalm\IssueBuffer;
use PhpParser;
class TypeChecker
{
protected $_absolute_class;
protected $_namespace;
protected $_checker;
protected $_check_nulls;
const ASSIGNMENT_TO_RIGHT = 1;
const ASSIGNMENT_TO_LEFT = -1;
public function __construct(StatementsSource $source, StatementsChecker $statements_checker)
{
$this->_absolute_class = $source->getAbsoluteClass();
$this->_namespace = $source->getNamespace();
$this->_checker = $statements_checker;
}
/**
* Gets all the type assertions in a conditional that are && together
* @param PhpParser\Node\Expr $conditional [description]
* @return array<string>
*/
public function getReconcilableTypeAssertions(PhpParser\Node\Expr $conditional)
{
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
$left_assertions = $this->getReconcilableTypeAssertions($conditional->left);
$right_assertions = $this->getReconcilableTypeAssertions($conditional->right);
$keys = array_intersect(array_keys($left_assertions), array_keys($right_assertions));
$if_types = [];
foreach ($keys as $key) {
if ($left_assertions[$key][0] !== '!' && $right_assertions[$key][0] !== '!') {
$if_types[$key] = $left_assertions[$key] . '|' . $right_assertions[$key];
}
}
return $if_types;
}
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd) {
$left_assertions = $this->getReconcilableTypeAssertions($conditional->left);
$right_assertions = $this->getReconcilableTypeAssertions($conditional->right);
return self::combineTypeAssertions($left_assertions, $right_assertions);
}
return $this->getTypeAssertions($conditional);
}
public function getNegatableTypeAssertions(PhpParser\Node\Expr $conditional)
{
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd) {
return [];
}
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
$left_assertions = $this->getNegatableTypeAssertions($conditional->left);
$right_assertions = $this->getNegatableTypeAssertions($conditional->right);
return self::combineTypeAssertions($left_assertions, $right_assertions);
}
return $this->getTypeAssertions($conditional);
}
private static function combineTypeAssertions(array $left_assertions, array $right_assertions)
{
$keys = array_merge(array_keys($left_assertions), array_keys($right_assertions));
$keys = array_unique($keys);
$if_types = [];
foreach ($keys as $key) {
if (isset($left_assertions[$key]) && isset($right_assertions[$key])) {
if ($left_assertions[$key][0] !== '!' && $right_assertions[$key][0] !== '!') {
$if_types[$key] = $left_assertions[$key] . '&' . $right_assertions[$key];
}
else {
$if_types[$key] = $right_assertions[$key];
}
}
else if (isset($left_assertions[$key])) {
$if_types[$key] = $left_assertions[$key];
}
else {
$if_types[$key] = $right_assertions[$key];
}
}
return $if_types;
}
/**
* Gets all the type assertions in a conditional
*
* @param PhpParser\Node\Expr $conditional
* @return array
*/
public function getTypeAssertions(PhpParser\Node\Expr $conditional)
{
$if_types = [];
if ($conditional instanceof PhpParser\Node\Expr\Instanceof_) {
$instanceof_type = $this->_getInstanceOfTypes($conditional);
if ($instanceof_type) {
$var_name = StatementsChecker::getVarId($conditional->expr);
if ($var_name) {
$if_types[$var_name] = $instanceof_type;
}
}
}
else if ($var_name = StatementsChecker::getVarId($conditional)) {
$if_types[$var_name] = '!empty';
}
else if ($conditional instanceof PhpParser\Node\Expr\Assign) {
$var_name = StatementsChecker::getVarId($conditional->var);
$if_types[$var_name] = '!empty';
}
else if ($conditional instanceof PhpParser\Node\Expr\BooleanNot) {
if ($conditional->expr instanceof PhpParser\Node\Expr\Instanceof_) {
$instanceof_type = $this->_getInstanceOfTypes($conditional->expr);
if ($instanceof_type) {
$var_name = StatementsChecker::getVarId($conditional->expr->expr);
if ($var_name) {
$if_types[$var_name] = '!' . $instanceof_type;
}
}
}
else if ($var_name = StatementsChecker::getVarId($conditional->expr)) {
$if_types[$var_name] = 'empty';
}
else if ($conditional->expr instanceof PhpParser\Node\Expr\Assign) {
$var_name = StatementsChecker::getVarId($conditional->expr->var);
$if_types[$var_name] = 'empty';
}
else if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\Identical || $conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\Equal) {
$null_position = self::_hasNullVariable($conditional->expr);
$false_position = self::_hasNullVariable($conditional->expr);
if ($null_position !== null) {
if ($null_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = StatementsChecker::getVarId($conditional->expr->left);
}
else if ($null_position === self::ASSIGNMENT_TO_LEFT) {
$var_name = StatementsChecker::getVarId($conditional->expr->right);
}
else {
throw new \InvalidArgumentException('Bad null variable position');
}
if ($var_name) {
if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
$if_types[$var_name] = '!null';
}
else {
// we do this because == null gives us a weaker idea than === null
$if_types[$var_name] = '!empty';
}
}
}
elseif ($false_position !== null) {
if ($false_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = StatementsChecker::getVarId($conditional->expr->left);
}
else if ($false_position === self::ASSIGNMENT_TO_LEFT) {
$var_name = StatementsChecker::getVarId($conditional->expr->right);
}
else {
throw new \InvalidArgumentException('Bad null variable position');
}
if ($var_name) {
if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
$if_types[$var_name] = '!false';
}
else {
// we do this because == null gives us a weaker idea than === null
$if_types[$var_name] = '!empty';
}
}
}
}
else if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical || $conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\NotEqual) {
$null_position = self::_hasNullVariable($conditional->expr);
$false_position = self::_hasNullVariable($conditional->expr);
if ($null_position !== null) {
if ($null_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = StatementsChecker::getVarId($conditional->expr->left);
}
else if ($null_position === self::ASSIGNMENT_TO_LEFT) {
$var_name = StatementsChecker::getVarId($conditional->expr->right);
}
else {
throw new \InvalidArgumentException('Bad null variable position');
}
if ($var_name) {
if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical) {
$if_types[$var_name] = 'null';
}
else {
$if_types[$var_name] = 'empty';
}
}
}
elseif ($false_position !== null) {
if ($false_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = StatementsChecker::getVarId($conditional->expr->left);
}
else if ($false_position === self::ASSIGNMENT_TO_LEFT) {
$var_name = StatementsChecker::getVarId($conditional->expr->right);
}
else {
throw new \InvalidArgumentException('Bad null variable position');
}
if ($var_name) {
if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
$if_types[$var_name] = 'false';
}
else {
// we do this because == null gives us a weaker idea than === null
$if_types[$var_name] = 'empty';
}
}
}
}
else if ($conditional->expr instanceof PhpParser\Node\Expr\Empty_) {
$var_name = StatementsChecker::getVarId($conditional->expr->expr);
if ($var_name) {
$if_types[$var_name] = '!empty';
}
}
elseif ($conditional->expr instanceof PhpParser\Node\Expr\FuncCall) {
if (self::_hasNullCheck($conditional->expr)) {
$var_name = StatementsChecker::getVarId($conditional->expr->args[0]->value);
$if_types[$var_name] = '!null';
}
else if (self::_hasIsACheck($conditional->expr)) {
$var_name = StatementsChecker::getVarId($conditional->expr->args[0]->value);
$if_types[$var_name] = '!' . $conditional->expr->args[1]->value->value;
}
else if (self::_hasArrayCheck($conditional->expr)) {
$var_name = StatementsChecker::getVarId($conditional->expr->args[0]->value);
$if_types[$var_name] = '!array';
}
else if (self::_hasBoolCheck($conditional->expr)) {
$var_name = StatementsChecker::getVarId($conditional->expr->args[0]->value);
$if_types[$var_name] = '!bool';
}
else if (self::_hasStringCheck($conditional->expr)) {
$var_name = StatementsChecker::getVarId($conditional->expr->args[0]->value);
$if_types[$var_name] = '!string';
}
else if (self::_hasObjectCheck($conditional->expr)) {
$var_name = StatementsChecker::getVarId($conditional->expr->args[0]->value);
$if_types[$var_name] = '!object';
}
}
else if ($conditional->expr instanceof PhpParser\Node\Expr\Isset_) {
foreach ($conditional->expr->vars as $isset_var) {
$var_name = StatementsChecker::getVarId($isset_var);
if ($var_name) {
$if_types[$var_name] = 'null';
}
}
}
}
else if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical || $conditional instanceof PhpParser\Node\Expr\BinaryOp\Equal) {
$null_position = self::_hasNullVariable($conditional);
$false_position = self::_hasFalseVariable($conditional);
if ($null_position !== null) {
if ($null_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = StatementsChecker::getVarId($conditional->left);
}
else if ($null_position === self::ASSIGNMENT_TO_LEFT) {
$var_name = StatementsChecker::getVarId($conditional->right);
}
else {
throw new \InvalidArgumentException('Bad null variable position');
}
if ($var_name) {
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
$if_types[$var_name] = 'null';
}
else {
$if_types[$var_name] = 'empty';
}
}
}
elseif ($false_position) {
if ($false_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = StatementsChecker::getVarId($conditional->left);
}
else if ($false_position === self::ASSIGNMENT_TO_LEFT) {
$var_name = StatementsChecker::getVarId($conditional->right);
}
else {
throw new \InvalidArgumentException('Bad null variable position');
}
if ($var_name) {
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
$if_types[$var_name] = 'false';
}
else {
$if_types[$var_name] = 'empty';
}
}
}
}
else if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical || $conditional instanceof PhpParser\Node\Expr\BinaryOp\NotEqual) {
$null_position = self::_hasNullVariable($conditional);
$false_position = self::_hasFalseVariable($conditional);
if ($null_position !== null) {
if ($null_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = StatementsChecker::getVarId($conditional->left);
}
else if ($null_position === self::ASSIGNMENT_TO_LEFT) {
$var_name = StatementsChecker::getVarId($conditional->right);
}
else {
throw new \InvalidArgumentException('Bad null variable position');
}
if ($var_name) {
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical) {
$if_types[$var_name] = '!null';
}
else {
$if_types[$var_name] = '!empty';
}
}
}
elseif ($false_position) {
if ($false_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = StatementsChecker::getVarId($conditional->left);
}
else if ($false_position === self::ASSIGNMENT_TO_LEFT) {
$var_name = StatementsChecker::getVarId($conditional->right);
}
else {
throw new \InvalidArgumentException('Bad null variable position');
}
if ($var_name) {
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
$if_types[$var_name] = '!false';
}
else {
$if_types[$var_name] = '!empty';
}
}
}
}
elseif ($conditional instanceof PhpParser\Node\Expr\FuncCall) {
if (self::_hasNullCheck($conditional)) {
$var_name = StatementsChecker::getVarId($conditional->args[0]->value);
$if_types[$var_name] = 'null';
}
else if (self::_hasIsACheck($conditional)) {
$var_name = StatementsChecker::getVarId($conditional->args[0]->value);
$if_types[$var_name] = $conditional->args[1]->value->value;
}
else if (self::_hasArrayCheck($conditional)) {
$var_name = StatementsChecker::getVarId($conditional->args[0]->value);
$if_types[$var_name] = 'array';
}
else if (self::_hasStringCheck($conditional)) {
$var_name = StatementsChecker::getVarId($conditional->args[0]->value);
$if_types[$var_name] = 'string';
}
else if (self::_hasBoolCheck($conditional)) {
$var_name = StatementsChecker::getVarId($conditional->args[0]->value);
$if_types[$var_name] = 'bool';
}
else if (self::_hasObjectCheck($conditional)) {
$var_name = StatementsChecker::getVarId($conditional->args[0]->value);
$if_types[$var_name] = 'object';
}
}
else if ($conditional instanceof PhpParser\Node\Expr\Empty_) {
$var_name = StatementsChecker::getVarId($conditional->expr);
if ($var_name) {
$if_types[$var_name] = 'empty';
}
}
else if ($conditional instanceof PhpParser\Node\Expr\Isset_) {
foreach ($conditional->vars as $isset_var) {
$var_name = StatementsChecker::getVarId($isset_var);
if ($var_name) {
$if_types[$var_name] = '!null';
}
}
}
return $if_types;
}
protected function _getInstanceOfTypes(PhpParser\Node\Expr\Instanceof_ $stmt)
{
if ($stmt->class instanceof PhpParser\Node\Name) {
if (!in_array($stmt->class->parts[0], ['self', 'static', 'parent'])) {
$instanceof_class = ClassChecker::getAbsoluteClassFromName($stmt->class, $this->_namespace, $this->_checker->getAliasedClasses());
return $instanceof_class;
} elseif ($stmt->class->parts === ['self']) {
return $this->_absolute_class;
}
}
return null;
}
protected static function _hasNullVariable(PhpParser\Node\Expr\BinaryOp $conditional)
{
if ($conditional->right instanceof PhpParser\Node\Expr\ConstFetch &&
$conditional->right->name instanceof PhpParser\Node\Name &&
$conditional->right->name->parts === ['null']) {
return self::ASSIGNMENT_TO_RIGHT;
}
if ($conditional->left instanceof PhpParser\Node\Expr\ConstFetch &&
$conditional->left->name instanceof PhpParser\Node\Name &&
$conditional->left->name->parts === ['null']) {
return self::ASSIGNMENT_TO_LEFT;
}
return null;
}
protected static function _hasFalseVariable(PhpParser\Node\Expr\BinaryOp $conditional)
{
if ($conditional->right instanceof PhpParser\Node\Expr\ConstFetch &&
$conditional->right->name instanceof PhpParser\Node\Name &&
$conditional->right->name->parts === ['false']) {
return self::ASSIGNMENT_TO_RIGHT;
}
if ($conditional->left instanceof PhpParser\Node\Expr\ConstFetch &&
$conditional->left->name instanceof PhpParser\Node\Name &&
$conditional->left->name->parts === ['false']) {
return self::ASSIGNMENT_TO_LEFT;
}
return null;
}
/**
* @return bool
*/
protected static function _hasNullCheck(PhpParser\Node\Expr\FuncCall $stmt)
{
if ($stmt->name instanceof PhpParser\Node\Name && $stmt->name->parts === ['is_null']) {
return true;
}
return false;
}
/**
* @return bool
*/
protected static function _hasIsACheck(PhpParser\Node\Expr\FuncCall $stmt)
{
if ($stmt->name instanceof PhpParser\Node\Name && $stmt->name->parts === ['is_a'] &&
$stmt->args[1]->value instanceof PhpParser\Node\Scalar\String_) {
return true;
}
return false;
}
/**
* @return bool
*/
protected static function _hasArrayCheck(PhpParser\Node\Expr\FuncCall $stmt)
{
if ($stmt->name instanceof PhpParser\Node\Name && $stmt->name->parts === ['is_array']) {
return true;
}
return false;
}
/**
* @return bool
*/
protected static function _hasStringCheck(PhpParser\Node\Expr\FuncCall $stmt)
{
if ($stmt->name instanceof PhpParser\Node\Name && $stmt->name->parts === ['is_string']) {
return true;
}
return false;
}
/**
* @return bool
*/
protected static function _hasBoolCheck(PhpParser\Node\Expr\FuncCall $stmt)
{
if ($stmt->name instanceof PhpParser\Node\Name && $stmt->name->parts === ['is_bool']) {
return true;
}
return false;
}
/**
* @return bool
*/
protected static function _hasObjectCheck(PhpParser\Node\Expr\FuncCall $stmt)
{
if ($stmt->name instanceof PhpParser\Node\Name && $stmt->name->parts === ['is_object']) {
return true;
}
return false;
}
/**
* Takes two arrays and consolidates them, removing null values from existing types where applicable
*
* @param array $new_types
* @param array $existing_types
* @return array|false
*/
public static function reconcileKeyedTypes(array $new_types, array $existing_types, $file_name, $line_number, array $suppressed_issues = [])
{
$keys = array_merge(array_keys($new_types), array_keys($existing_types));
$keys = array_unique($keys);
$result_types = [];
if (empty($new_types)) {
return $existing_types;
}
foreach ($keys as $key) {
if (!isset($new_types[$key])) {
$result_types[$key] = $existing_types[$key];
continue;
}
$new_type_parts = explode('&', $new_types[$key]);
$result_type = isset($existing_types[$key]) ? clone $existing_types[$key] : null;
foreach ($new_type_parts as $new_type_part) {
$result_type = self::reconcileTypes(
(string) $new_type_part,
$result_type,
$key,
$file_name,
$line_number,
$suppressed_issues
);
}
//echo((string) $new_types[$key] . ' and ' . (isset($existing_types[$key]) ? (string) $existing_types[$key] : '') . ' => ' . $result_type . PHP_EOL);
if ($result_type === false) {
return false;
}
$result_types[$key] = $result_type;
}
return $result_types;
}
/**
* Reconciles types
*
* think of this as a set of functions e.g. empty(T), notEmpty(T), null(T), notNull(T) etc. where
* empty(Object) => null,
* empty(bool) => false,
* notEmpty(Object|null) => Object,
* notEmpty(Object|false) => Object
*
* @param string $new_var_type
* @param Type\Union $existing_var_type
* @param string $key
* @param string $file_name
* @param int $line_number
* @return Type\Union|false
*/
public static function reconcileTypes($new_var_type, Type\Union $existing_var_type = null, $key = null, $file_name = null, $line_number = null, array $suppressed_issues = [])
{
$result_var_types = null;
if (!$existing_var_type) {
return Type::getMixed();
}
if ($new_var_type === 'mixed' && $existing_var_type->isMixed()) {
return $existing_var_type;
}
if ($new_var_type === 'null') {
return Type::getNull();
}
if ($new_var_type[0] === '!') {
if (in_array($new_var_type, ['!empty', '!null'])) {
$existing_var_type->removeType('null');
if ($new_var_type === '!empty') {
$existing_var_type->removeType('false');
}
if (empty($existing_var_type->types)) {
// @todo - I think there's a better way to handle this, but for the moment
// mixed will have to do.
return Type::getMixed();
}
return $existing_var_type;
}
$negated_type = substr($new_var_type, 1);
$existing_var_type->removeType($negated_type);
if (empty($existing_var_type->types)) {
if ($key) {
if (IssueBuffer::accepts(
new FailedTypeResolution('Cannot resolve types for ' . $key, $file_name, $line_number),
$suppressed_issues
)) {
return false;
}
return Type::getMixed();
}
}
return $existing_var_type;
}
if ($new_var_type === 'empty') {
if ($existing_var_type->hasType('bool')) {
$existing_var_type->removeType('bool');
$existing_var_type->types['false'] = Type::getFalse(false);
}
$existing_var_type->removeObjects();
if (empty($existing_var_type->types)) {
return Type::getNull();
}
return $existing_var_type;
}
return Type::parseString($new_var_type);
}
public static function isNegation($type, $existing_type)
{
if ($type === 'mixed' || $existing_type === 'mixed') {
return false;
}
if ($type === '!' . $existing_type || $existing_type === '!' . $type) {
return true;
}
if (in_array($type, ['empty', 'false', 'null']) && !in_array($existing_type, ['empty', 'false', 'null'])) {
return true;
}
if (in_array($existing_type, ['empty', 'false', 'null']) && !in_array($type, ['empty', 'false', 'null'])) {
return true;
}
return false;
}
/**
* Takes two arrays of types and merges them
*
* @param array<UnionType> $new_types
* @param array<UnionType> $existing_types
* @return array
*/
public static function combineKeyedTypes(array $new_types, array $existing_types)
{
$keys = array_merge(array_keys($new_types), array_keys($existing_types));
$keys = array_unique($keys);
$result_types = [];
if (empty($new_types)) {
return $existing_types;
}
if (empty($existing_types)) {
return $new_types;
}
foreach ($keys as $key) {
if (!isset($existing_types[$key])) {
$result_types[$key] = $new_types[$key];
continue;
}
if (!isset($new_types[$key])) {
$result_types[$key] = $existing_types[$key];
continue;
}
$existing_var_types = $existing_types[$key];
$new_var_types = $new_types[$key];
if ((string) $new_var_types === (string) $existing_var_types) {
$result_types[$key] = $new_var_types;
}
else {
$result_types[$key] = Type::combineUnionTypes($new_var_types, $existing_var_types);
}
}
return $result_types;
}
public static function reduceTypes(array $all_types)
{
if (in_array('mixed', $all_types)) {
return ['mixed'];
}
$array_types = array_filter($all_types, function($type) {
return preg_match('/^array(\<|$)/', $type);
});
$all_types = array_flip($all_types);
if (isset($all_types['array<empty>']) && count($array_types) > 1) {
unset($all_types['array<empty>']);
}
if (isset($all_types['array<mixed>'])) {
unset($all_types['array<mixed>']);
$all_types['array'] = true;
}
return array_keys($all_types);
}
public static function negateTypes(array $types)
{
return array_map(function ($type) {
if ($type === 'mixed') {
return $type;
}
$type_parts = explode('&', $type);
foreach ($type_parts as &$type_part) {
$type_part = $type_part[0] === '!' ? substr($type_part, 1) : '!' . $type_part;
}
return implode('&', $type_parts);
}, $types);
}
public static function hasIdenticalTypes(Type\Union $declared_type, Type\Union $inferred_type, $absolute_class)
{
if ($declared_type->isNullable() !== $inferred_type->isNullable()) {
return false;
}
$inferred_type = StatementsChecker::fleshOutTypes($inferred_type, [], $absolute_class, '');
$simple_declared_types = array_filter(array_keys($declared_type->types), function($type_value) { return $type_value !== 'null'; });
$simple_inferred_types = array_filter(array_keys($inferred_type->types), function($type_value) { return $type_value !== 'null'; });
// gets elements A△B
$differing_types = array_diff($simple_inferred_types, $simple_declared_types);
if (count($differing_types)) {
// check whether the differing types are subclasses of declared return types
$truly_different = false;
foreach ($differing_types as $differing_type) {
$is_match = false;
if ($differing_type === 'mixed') {
continue;
}
foreach ($simple_declared_types as $simple_declared_type) {
if (($simple_declared_type === 'object' && ClassChecker::classOrInterfaceExists($differing_type)) ||
ClassChecker::classExtendsOrImplements($differing_type, $simple_declared_type) ||
(in_array($differing_type, ['float', 'double', 'int']) && in_array($simple_declared_type, ['float', 'double', 'int'])) ||
(in_array($differing_type, ['boolean', 'bool']) && in_array($simple_declared_type, ['boolean', 'bool']))
) {
$is_match = true;
break;
}
}
if (!$is_match) {
$truly_different = true;
}
}
return !$truly_different;
}
foreach ($declared_type->types as $key => $declared_atomic_type) {
if (!isset($inferred_type->types[$key])) {
continue;
}
$inferred_atomic_type = $inferred_type->types[$key];
if (!($declared_atomic_type instanceof Type\Generic)) {
continue;
}
if (!($inferred_atomic_type instanceof Type\Generic) && $declared_atomic_type instanceof Type\Generic) {
// @todo handle this better
continue;
}
if (!self::hasIdenticalTypes($declared_atomic_type->type_params[0], $inferred_atomic_type->type_params[0], $absolute_class)) {
return false;
}
}
return true;
}
}