From ad5ec9501de08611399829a01534721f3d5d2979 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Sun, 29 Nov 2020 17:40:52 -0500 Subject: [PATCH] Ensure class template types are mapped to static methods where necessary Ref #4733 --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 2 +- .../FunctionLike/ReturnTypeAnalyzer.php | 2 +- .../Call/ClassTemplateParamCollector.php | 10 +-- .../Call/Method/AtomicMethodCallAnalyzer.php | 2 +- .../ExistingAtomicMethodCallAnalyzer.php | 4 +- .../ExistingAtomicStaticCallAnalyzer.php | 2 +- .../Analyzer/Statements/ReturnAnalyzer.php | 2 +- tests/Template/ClassTemplateTest.php | 83 +++++++++++++++++++ 8 files changed, 95 insertions(+), 12 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 4323677e9..c53937778 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -1071,7 +1071,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer $storage, null, new Type\Atomic\TNamedObject($fq_class_name), - '$this' + true ); $template_result = new \Psalm\Internal\Type\TemplateResult( diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php index 83db2649f..b9cee3f07 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php @@ -802,7 +802,7 @@ class ReturnTypeAnalyzer $codebase->classlike_storage_provider->get($context->self), strtolower($function->name->name), new Type\Atomic\TNamedObject($context->self), - '$this' + true ); $class_template_params = $class_template_params ?: []; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php index 2596a7336..c33d4cfe2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php @@ -28,7 +28,7 @@ class ClassTemplateParamCollector ClassLikeStorage $static_class_storage, ?string $method_name = null, ?Type\Atomic $lhs_type_part = null, - ?string $lhs_var_id = null + bool $self_call = false ): ?array { $static_fq_class_name = $static_class_storage->name; @@ -96,7 +96,7 @@ class ClassTemplateParamCollector foreach ($static_class_storage->template_types as $type_name => $_) { if (isset($lhs_type_part->type_params[$i])) { - if ($lhs_var_id !== '$this' || $static_fq_class_name !== $static_class_storage->name) { + if (!$self_call || $static_fq_class_name !== $static_class_storage->name) { $class_template_params[$type_name][$static_class_storage->name] = $lhs_type_part->type_params[$i]; } @@ -183,13 +183,13 @@ class ClassTemplateParamCollector } } - if ($lhs_var_id !== '$this' || $static_fq_class_name !== $class_storage->name) { + if (!$self_call || $static_fq_class_name !== $class_storage->name) { $class_template_params[$type_name][$class_storage->name] = $output_type_extends ?: Type::getMixed(); } } - if (($lhs_var_id !== '$this' || $static_fq_class_name !== $class_storage->name) + if ((!$self_call || $static_fq_class_name !== $class_storage->name) && !isset($class_template_params[$type_name]) ) { $class_template_params[$type_name] = [$class_storage->name => Type::getMixed()]; @@ -216,7 +216,7 @@ class ClassTemplateParamCollector } } - if ($lhs_var_id !== '$this') { + if (!$self_call) { if (!isset($class_template_params[$type_name])) { $class_template_params[$type_name][$class_storage->name] = $type; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php index b1111f408..05a1ddaef 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -730,7 +730,7 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer $codebase->classlike_storage_provider->get($fq_class_name), null, $lhs_type_part, - $lhs_var_id + $lhs_var_id === '$this' ); $lhs_type_part = clone $mixin; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php index 3951d56e5..413918e08 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -145,7 +145,7 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer $class_storage, $method_name_lc, $lhs_type_part, - $lhs_var_id + $lhs_var_id === '$this' ); if ($lhs_var_id === '$this' && $parent_source instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) { @@ -167,7 +167,7 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer $class_storage, $method_name_lc, $lhs_type_part, - $lhs_var_id + $lhs_var_id === '$this' ); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index 5015356e9..764a302a1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -130,7 +130,7 @@ class ExistingAtomicStaticCallAnalyzer $class_storage, $method_name_lc, $lhs_type_part, - null + !$statements_analyzer->isStatic() && $method_id->fq_class_name === $context->self ); if ($found_generic_params diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index 2602873df..1e8d29ded 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -259,7 +259,7 @@ class ReturnAnalyzer $class_storage, strtolower($method_name), null, - '$this' + true ); if ($found_generic_params) { diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index 0fb0ffd92..46b7c2fed 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -3105,6 +3105,89 @@ class ClassTemplateTest extends TestCase takesFMixed($f); }' ], + 'arrayCollectionMapInternal' => [ + ' */ + private $elements; + + /** @psalm-param array $elements */ + public function __construct(array $elements = []) + { + $this->elements = $elements; + } + + /** + * @template TNewKey of array-key + * @template TNew + * @psalm-param array $elements + * @psalm-return static + */ + protected static function createFrom(array $elements) + { + return new static($elements); + } + + /** + * @psalm-template U + * @psalm-param Closure(T=):U $func + * @psalm-return static + */ + public function map(Closure $func) + { + $new_elements = array_map($func, $this->elements); + return self::createFrom($new_elements); + } + }' + ], + 'arrayCollectionMapExternal' => [ + ' */ + private $elements; + + /** @psalm-param array $elements */ + public function __construct(array $elements = []) + { + $this->elements = $elements; + } + + /** + * @psalm-template U + * @psalm-param Closure(T=):U $func + * @psalm-return ArrayCollection + */ + public function map(Closure $func) + { + $new_elements = array_map($func, $this->elements); + return Creator::createFrom($new_elements); + } + } + + class Creator { + /** + * @template TNewKey of array-key + * @template TNew + * @psalm-param array $elements + * @psalm-return ArrayCollection + */ + public static function createFrom(array $elements) { + return new ArrayCollection($elements); + } + }' + ], ]; }