Skip to content

Commit

Permalink
Improve handling of generic params in intersection types
Browse files Browse the repository at this point in the history
Ref #1053
  • Loading branch information
muglug committed Nov 2, 2018
1 parent f592e54 commit f7a37d0
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 37 deletions.
14 changes: 9 additions & 5 deletions src/Psalm/Checker/Statements/Block/ForeachChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public static function analyze(
return false;
}
}

if (TypeChecker::isAtomicContainedBy(
$codebase,
$iterator_type,
Expand All @@ -184,12 +184,16 @@ public static function analyze(
}

foreach ($iterator_types as $iterator_type) {
if ($iterator_type instanceof Type\Atomic\TGenericParam) {
throw new \UnexpectedValueException('Shouldn’t get a generic param here');
}

$has_valid_iterator = true;

if ($iterator_type instanceof Type\Atomic\TGenericObject &&
(strtolower($iterator_type->value) === 'iterable' ||
strtolower($iterator_type->value) === 'traversable' ||
$codebase->classImplements(
if ($iterator_type instanceof Type\Atomic\TGenericObject
&& (strtolower($iterator_type->value) === 'iterable'
|| strtolower($iterator_type->value) === 'traversable'
|| $codebase->classImplements(
$iterator_type->value,
'Traversable'
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,10 @@ function (PhpParser\Node\Arg $arg) {

if ($intersection_types && !$codebase->methodExists($method_id)) {
foreach ($intersection_types as $intersection_type) {
if ($intersection_type instanceof Type\Atomic\TGenericParam) {
throw new \UnexpectedValueException('Shouldn’t get a generic param here');
}

$method_id = $intersection_type->value . '::' . $method_name_lc;
$fq_class_name = $intersection_type->value;

Expand Down
3 changes: 3 additions & 0 deletions src/Psalm/Checker/Statements/Expression/CallChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -1997,6 +1997,9 @@ public static function getFunctionIdsFromCallableArg(

if ($type_part->extra_types) {
foreach ($type_part->extra_types as $extra_type) {
if ($extra_type instanceof Type\Atomic\TGenericParam) {
throw new \UnexpectedValueException('Shouldn’t get a generic param here');
}
$method_id .= '&' . $extra_type->value . '::' . $method_name_arg->value;
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/Psalm/Checker/TypeChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,17 @@ private static function isObjectContainedByObject(
$intersection_container_types[] = $container_type_part;

foreach ($intersection_container_types as $intersection_container_type) {
if ($intersection_container_type instanceof TGenericParam) {
continue;
}

$intersection_container_type_lower = strtolower($intersection_container_type->value);

foreach ($intersection_input_types as $intersection_input_type) {
if ($intersection_input_type instanceof TGenericParam) {
continue;
}

$intersection_input_type_lower = strtolower($intersection_input_type->value);

if ($intersection_container_type_lower === $intersection_input_type_lower) {
Expand Down
7 changes: 5 additions & 2 deletions src/Psalm/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TGenericParam;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TLiteralClassString;
use Psalm\Type\Atomic\TLiteralFloat;
Expand Down Expand Up @@ -271,12 +272,14 @@ function (ParseTree $child_tree) use ($template_type_names) {
);

foreach ($intersection_types as $intersection_type) {
if (!$intersection_type instanceof TNamedObject) {
if (!$intersection_type instanceof TNamedObject
&& !$intersection_type instanceof TGenericParam
) {
throw new TypeParseTreeException('Intersection types must all be objects');
}
}

/** @var TNamedObject[] $intersection_types */
/** @var array<int, TNamedObject|TGenericParam> $intersection_types */
$first_type = array_shift($intersection_types);

$first_type->extra_types = $intersection_types;
Expand Down
54 changes: 54 additions & 0 deletions src/Psalm/Type/Atomic/HasIntersectionTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
namespace Psalm\Type\Atomic;

use Psalm\Codebase;
use Psalm\Storage\FunctionLikeParameter;
use Psalm\Type\Atomic;
use Psalm\Type\Union;

trait HasIntersectionTrait
{
/**
* @var array<int, TNamedObject|TGenericParam>|null
*/
public $extra_types;

/**
* @param string|null $namespace
* @param array<string> $aliased_classes
* @param string|null $this_class
* @param bool $use_phpdoc_format
*
* @return string
*/
private function getNamespacedIntersectionTypes($namespace, array $aliased_classes, $this_class, $use_phpdoc_format)
{
if (!$this->extra_types) {
return '';
}

return '&' . implode(
'&',
array_map(
/**
* @param TNamedObject|TGenericParam $extra_type
* @return string
*/
function (Atomic $extra_type) use (
$namespace,
$aliased_classes,
$this_class,
$use_phpdoc_format
) {
return $extra_type->toNamespacedString(
$namespace,
$aliased_classes,
$this_class,
$use_phpdoc_format
);
},
$this->extra_types
)
);
}
}
22 changes: 22 additions & 0 deletions src/Psalm/Type/Atomic/TGenericParam.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

class TGenericParam extends \Psalm\Type\Atomic
{
use HasIntersectionTrait;

/**
* @var string
*/
Expand Down Expand Up @@ -53,6 +55,26 @@ public function toPhpString(
return null;
}

/**
* @param string|null $namespace
* @param array<string> $aliased_classes
* @param string|null $this_class
* @param bool $use_phpdoc_format
*
* @return string
*/
public function toNamespacedString($namespace, array $aliased_classes, $this_class, $use_phpdoc_format)
{
$intersection_types = $this->getNamespacedIntersectionTypes(
$namespace,
$aliased_classes,
$this_class,
$use_phpdoc_format
);

return $this->param_name . $intersection_types;
}

/**
* @return bool
*/
Expand Down
73 changes: 43 additions & 30 deletions src/Psalm/Type/Atomic/TNamedObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,17 @@
namespace Psalm\Type\Atomic;

use Psalm\Type\Atomic;
use Psalm\Type\Union;

class TNamedObject extends Atomic
{
use HasIntersectionTrait;

/**
* @var string
*/
public $value;

/**
* @var TNamedObject[]|null
*/
public $extra_types;

/**
* @param string $value the name of the object
*/
Expand Down Expand Up @@ -57,30 +55,12 @@ public function toNamespacedString($namespace, array $aliased_classes, $this_cla
$class_parts = explode('\\', $this->value);
$class_name = array_pop($class_parts);

$intersection_types = $this->extra_types
? '&' . implode(
'&',
array_map(
/**
* @return string
*/
function (TNamedObject $extra_type) use (
$namespace,
$aliased_classes,
$this_class,
$use_phpdoc_format
) {
return $extra_type->toNamespacedString(
$namespace,
$aliased_classes,
$this_class,
$use_phpdoc_format
);
},
$this->extra_types
)
)
: '';
$intersection_types = $this->getNamespacedIntersectionTypes(
$namespace,
$aliased_classes,
$this_class,
$use_phpdoc_format
);

if ($this->value === $this_class) {
return 'self' . $intersection_types;
Expand Down Expand Up @@ -136,10 +116,43 @@ public function addIntersectionType(TNamedObject $type)
}

/**
* @return TNamedObject[]|null
* @return array<int, TNamedObject|TGenericParam>|null
*/
public function getIntersectionTypes()
{
return $this->extra_types;
}

/**
* @param array<string, Union> $template_types
*
* @return void
*/
public function replaceTemplateTypesWithArgTypes(array $template_types)
{
if (!$this->extra_types) {
return;
}

$keys_to_unset = [];

$new_types = [];

foreach ($this->extra_types as $i => $extra_type) {
if ($extra_type instanceof TGenericParam && isset($template_types[$extra_type->param_name])) {
$keys_to_unset[] = $i;

This comment has been minimized.

Copy link
@weirdan

weirdan Nov 2, 2018

Collaborator

Seems to be unused.

This comment has been minimized.

Copy link
@muglug

muglug Nov 2, 2018

Author Collaborator

Thanks, fixed in latest commit

$template_type = clone $template_types[$extra_type->param_name];

foreach ($template_type->getTypes() as $template_type_part) {
if ($template_type_part instanceof TNamedObject) {
$new_types[] = $template_type_part;
}
}
} else {
$new_types[] = $extra_type;
}
}

$this->extra_types = $new_types;
}
}

0 comments on commit f7a37d0

Please sign in to comment.