Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional test and fix when comparing nested templates #9095

Merged
merged 2 commits into from
Jan 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 36 additions & 14 deletions src/Psalm/Internal/Type/Comparator/ObjectComparator.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use function in_array;
use function strpos;
use function strtolower;
use function substr;

/**
* @internal
Expand All @@ -37,22 +38,43 @@ public static function isShallowlyContainedBy(
if ($container_type_part instanceof TTemplateParam
&& $input_type_part instanceof TTemplateParam
&& $container_type_part->defining_class != $input_type_part->defining_class
&& strpos($container_type_part->defining_class, 'fn-') !== 0
&& strpos($input_type_part->defining_class, 'fn-') !== 0
&& 1 == count($container_type_part->as->getAtomicTypes())
&& 1 == count($input_type_part->as->getAtomicTypes())) {
$containerAs = current($container_type_part->as->getAtomicTypes());
$inputAs = current($input_type_part->as->getAtomicTypes());
if ($containerAs instanceof TNamedObject && $inputAs instanceof TNamedObject) {
return self::isShallowlyContainedBy(
$codebase,
$inputAs,
$containerAs,
$allow_interface_equality,
$atomic_comparison_result,
);
} elseif ($containerAs instanceof TMixed && $inputAs instanceof TMixed) {
return true;
$containerDefinedInFunction = strpos($container_type_part->defining_class, 'fn-') === 0;
$inputDefinedInFunction = strpos($input_type_part->defining_class, 'fn-') === 0;
if ($inputDefinedInFunction) {
$separatorPos = strpos($input_type_part->defining_class, '::');
if ($separatorPos === false) {
// Is that possible ? Falling back to default definition.
$inputDefiningClass = $input_type_part->defining_class;
} else {
$inputDefiningClass = substr($input_type_part->defining_class, 3, $separatorPos - 3);
}
} else {
$inputDefiningClass = $input_type_part->defining_class;
}

// FIXME Missing analysis for additional cases, for example :
// - input from a parameter in a static function that is defined in the container class
// - input and container are both defined on function parameters
if ((!$inputDefinedInFunction
&& !$containerDefinedInFunction)
|| ($inputDefinedInFunction
&& !$containerDefinedInFunction
&& strtolower($inputDefiningClass) != strtolower($container_type_part->defining_class))) {
$containerAs = current($container_type_part->as->getAtomicTypes());
$inputAs = current($input_type_part->as->getAtomicTypes());
if ($containerAs instanceof TNamedObject && $inputAs instanceof TNamedObject) {
return self::isShallowlyContainedBy(
$codebase,
$inputAs,
$containerAs,
$allow_interface_equality,
$atomic_comparison_result,
);
} elseif ($containerAs instanceof TMixed && $inputAs instanceof TMixed) {
return true;
}
}
}

Expand Down
98 changes: 98 additions & 0 deletions tests/Template/NestedTemplateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,104 @@ protected function getDbRepo(): DbEntityRepository
abstract class AnObjectDbRepositoryWrapper
extends DbRepositoryWrapper {}',
],
'4levelNestedTemplateAsFunctionParameter' => [
'code' => '<?php
/**
* Interface for all DB entities that map to some data-model object.
*
* @template T
*/
interface DbEntity
{
/**
* Maps this entity to a data-model entity
*
* @return T Data-model entity to which this DB entity maps.
*/
public function toCore();
}

/**
* @template T of object
*/
abstract class EntityRepository {}

/**
* Base entity repository with common tooling.
*
* @template T of object
* @extends EntityRepository<T>
*/
abstract class DbEntityRepository
extends EntityRepository {}

interface ObjectId {}

/**
* @template I of ObjectId
*/
interface AnObject {}

/**
* Base entity repository with common tooling.
*
* @template I of ObjectId
* @template O of AnObject<I>
* @template E of DbEntity<O>
* @extends DbEntityRepository<E>
*/
abstract class AnObjectEntityRepository
extends DbEntityRepository
{}

/**
* Base repository implementation backed by a Db repository.
*
* @template T
* @template E of DbEntity<T>
* @template R of DbEntityRepository<E>
*/
abstract class DbRepositoryWrapper
{
/** @var R $repo Db repository */
private DbEntityRepository $repo;

/**
* Getter for the Db repository.
*
* @return DbEntityRepository The Db repository.
* @psalm-return R
*/
protected function getDbRepo(): DbEntityRepository
{
return $this->repo;
}
}

/**
* Base implementation for all custom repositories that map to Core objects.
*
* @template I of ObjectId
* @template O of AnObject<I>
* @template E of DbEntity<O>
* @template R of AnObjectEntityRepository<I, O, E>
* @extends DbRepositoryWrapper<O, E, R>
*/
abstract class AnObjectDbRepositoryWrapper
extends DbRepositoryWrapper {}

abstract class Utilities {
/**
* @template I of ObjectId
* @template O of AnObject<I>
* @template E of DbEntity<O>
* @template R of AnObjectEntityRepository<I, O, E>
* @psalm-param AnObjectDbRepositoryWrapper<I, O, E, R> $repo
* @return void
*/
abstract public static function doSomething(AnObjectDbRepositoryWrapper $repo): void;
}',
],
];
}

Expand Down