diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php index a277406d062..a3bd4b3b493 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -183,6 +183,7 @@ public static function fetch( $self_fq_class_name, $statements_analyzer, $args, + $template_result, ); if ($return_type_candidate) { @@ -449,7 +450,7 @@ public static function taintMethodCallResult( $stmt_var_type = $context->vars_in_scope[$var_id]->setParentNodes( $var_nodes, ); - + $context->vars_in_scope[$var_id] = $stmt_var_type; } else { $method_call_node = DataFlowNode::getForMethodReturn( diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index ff78e6842b1..2d276bf2f24 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -557,7 +557,8 @@ public function getMethodReturnType( MethodIdentifier $method_id, ?string &$self_class, ?SourceAnalyzer $source_analyzer = null, - ?array $args = null + ?array $args = null, + ?TemplateResult $template_result = null ): ?Union { $original_fq_class_name = $method_id->fq_class_name; $original_method_name = $method_id->method_name; @@ -784,9 +785,18 @@ public function getMethodReturnType( ); if ($found_generic_params) { + $passed_template_result = $template_result; + $template_result = new TemplateResult( + [], + $found_generic_params, + ); + if ($passed_template_result !== null) { + $template_result = $template_result->merge($passed_template_result); + } + $overridden_storage_return_type = TemplateInferredTypeReplacer::replace( $overridden_storage_return_type, - new TemplateResult([], $found_generic_params), + $template_result, $source_analyzer->getCodebase(), ); } diff --git a/src/Psalm/Internal/Type/TemplateResult.php b/src/Psalm/Internal/Type/TemplateResult.php index 35d98333479..72a7a05cd5e 100644 --- a/src/Psalm/Internal/Type/TemplateResult.php +++ b/src/Psalm/Internal/Type/TemplateResult.php @@ -4,6 +4,9 @@ use Psalm\Type\Union; +use function array_merge; +use function array_replace_recursive; + /** * This class captures the result of running Psalm's argument analysis with * regard to generic parameters. @@ -63,4 +66,19 @@ public function __construct(array $template_types, array $lower_bounds) } } } + + public function merge(TemplateResult $result): TemplateResult + { + if ($result === $this) { + return $this; + } + + $instance = clone $this; + /** @var array>> $lower_bounds */ + $lower_bounds = array_replace_recursive($instance->lower_bounds, $result->lower_bounds); + $instance->lower_bounds = $lower_bounds; + $instance->template_types = array_merge($instance->template_types, $result->template_types); + + return $instance; + } } diff --git a/tests/Template/ConditionalReturnTypeTest.php b/tests/Template/ConditionalReturnTypeTest.php index cafcb7b8668..18669470af7 100644 --- a/tests/Template/ConditionalReturnTypeTest.php +++ b/tests/Template/ConditionalReturnTypeTest.php @@ -885,6 +885,72 @@ function getSomethingElse() 'ignored_issues' => [], 'php_version' => '7.2', ], + 'ineritedConditionalTemplatedReturnType' => [ + 'code' => '|string $name + * @return ($name is class-string ? TRequestedInstance : InstanceType) + */ + public function build(string $name): mixed; + } + + /** + * @template InstanceType + * @template-implements ContainerInterface + */ + abstract class MixedContainer implements ContainerInterface + { + /** @param InstanceType $instance */ + public function __construct(private readonly mixed $instance) + {} + + public function build(string $name): mixed + { + return $this->instance; + } + } + + /** + * @template InstanceType of object + * @template-extends MixedContainer + */ + abstract class ObjectContainer extends MixedContainer + { + public function build(string $name): object + { + return parent::build($name); + } + } + + /** @template-extends ObjectContainer */ + final class SpecificObjectContainer extends ObjectContainer + { + } + + final class SpecificObject extends stdClass {} + + $container = new SpecificObjectContainer(new stdClass()); + $object = $container->build(SpecificObject::class); + $nonSpecificObject = $container->build("whatever"); + + /** @var ObjectContainer $container */ + $container = null; + $justObject = $container->build("whatever"); + $specificObject = $container->build(stdClass::class); + ', + 'assertions' => [ + '$object===' => 'SpecificObject', + '$nonSpecificObject===' => 'stdClass', + '$justObject===' => 'object', + '$specificObject===' => 'stdClass', + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } }