diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 891cd1206c6..99a11cbb337 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -24,6 +24,9 @@ use Psalm\Internal\Provider\NodeDataProvider; use Psalm\Internal\Type\Comparator\TypeComparisonResult; use Psalm\Internal\Type\Comparator\UnionTypeComparator; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; +use Psalm\Internal\Type\TemplateResult; +use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\InvalidDocblockParamName; use Psalm\Issue\InvalidParamDefault; @@ -64,6 +67,7 @@ use function end; use function in_array; use function is_string; +use function mb_strpos; use function md5; use function microtime; use function reset; @@ -1808,10 +1812,30 @@ private function getFunctionInformation( } else { $this_object_type = new TNamedObject($context->self); } - + $this_object_type->was_static = !$storage->final; - $context->vars_in_scope['$this'] = new Union([$this_object_type]); + if ($this->storage instanceof MethodStorage && $this->storage->if_this_is_type) { + $template_result = new TemplateResult($this->getTemplateTypeMap() ?? [], []); + + TemplateStandinTypeReplacer::replace( + new Union([$this_object_type]), + $template_result, + $codebase, + null, + $this->storage->if_this_is_type + ); + + foreach ($context->vars_in_scope as $var_name => $var_type) { + if (0 === mb_strpos($var_name, '$this->')) { + TemplateInferredTypeReplacer::replace($var_type, $template_result, $codebase); + } + } + + $context->vars_in_scope['$this'] = $this->storage->if_this_is_type; + } else { + $context->vars_in_scope['$this'] = new Union([$this_object_type]); + } if ($codebase->taint_flow_graph && $storage->specialize_call 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 12eeff49882..1fda720d0c2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -20,7 +20,9 @@ use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\Comparator\TypeComparisonResult; use Psalm\Internal\Type\Comparator\UnionTypeComparator; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; +use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\IfThisIsMismatch; use Psalm\Issue\InvalidPropertyAssignmentValue; @@ -188,7 +190,32 @@ public static function analyze( } } + $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id); + + try { + $method_storage = $codebase->methods->getStorage($declaring_method_id ?? $method_id); + } catch (UnexpectedValueException $e) { + $method_storage = null; + } + + $method_template_params = []; + + if ($method_storage && $method_storage->if_this_is_type) { + $method_template_result = new TemplateResult($method_storage->template_types ?: [], []); + + TemplateStandinTypeReplacer::replace( + clone $method_storage->if_this_is_type, + $method_template_result, + $codebase, + null, + new Union([$lhs_type_part]) + ); + + $method_template_params = $method_template_result->lower_bounds; + } + $template_result = new TemplateResult([], $class_template_params ?: []); + $template_result->lower_bounds += $method_template_params; if ($codebase->store_node_types && !$context->collect_initializations @@ -215,8 +242,6 @@ public static function analyze( return Type::getMixed(); } - $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id); - $return_type_candidate = MethodCallReturnTypeFetcher::fetch( $statements_analyzer, $codebase, @@ -264,30 +289,23 @@ public static function analyze( } } - try { - $method_storage = $codebase->methods->getStorage($declaring_method_id ?? $method_id); - } catch (UnexpectedValueException $e) { - $method_storage = null; - } - if ($method_storage) { - $class_type = new Union([$lhs_type_part]); - - if ($method_storage->if_this_is_type - && !UnionTypeComparator::isContainedBy( - $codebase, - $class_type, - $method_storage->if_this_is_type - ) - ) { - IssueBuffer::maybeAdd( - new IfThisIsMismatch( - 'Class type must be ' . $method_storage->if_this_is_type->getId() - . ' current type ' . $class_type->getId(), - new CodeLocation($source, $stmt->name) - ), - $statements_analyzer->getSuppressedIssues() - ); + if ($method_storage->if_this_is_type) { + $class_type = new Union([$lhs_type_part]); + $if_this_is_type = clone $method_storage->if_this_is_type; + + TemplateInferredTypeReplacer::replace($if_this_is_type, $template_result, $codebase); + + if (!UnionTypeComparator::isContainedBy($codebase, $class_type, $if_this_is_type)) { + IssueBuffer::maybeAdd( + new IfThisIsMismatch( + 'Class type must be ' . $method_storage->if_this_is_type->getId() + . ' current type ' . $class_type->getId(), + new CodeLocation($source, $stmt->name) + ), + $statements_analyzer->getSuppressedIssues() + ); + } } if ($method_storage->self_out_type && $lhs_var_id) { diff --git a/tests/IfThisIsTest.php b/tests/IfThisIsTest.php index b8f4bcfdf43..2c3c3b004e2 100644 --- a/tests/IfThisIsTest.php +++ b/tests/IfThisIsTest.php @@ -134,6 +134,136 @@ public function start(): void $app->start(); ' ], + 'ifThisIsChangeThisTypeInsideMethod' => [ + ' */ + private $items; + + /** + * @param list $items + */ + public function __construct(array $items) + { + $this->items = $items; + } + + /** + * @psalm-if-this-is ArrayList> + * @return ArrayList + */ + public function compact(): ArrayList + { + $values = []; + + foreach ($this->items as $item) { + $value = $item->unwrap(); + + if (null !== $value) { + $values[] = $value; + } + } + + return new self($values); + } + } + + /** @var ArrayList> $list */ + $list = new ArrayList([]); + $numbers = $list->compact(); + ', + 'assertions' => [ + '$numbers' => 'ArrayList' + ], + ], + 'ifThisIsResolveTemplateParams' => [ + ' */ + private $items; + + /** + * @param list $items + */ + public function __construct(array $items) + { + $this->items = $items; + } + + /** + * @template A + * @template B + * @template TOption of Option + * @template TEither of Either + * + * @psalm-if-this-is ArrayList + * @return ArrayList + */ + public function compact(): ArrayList + { + $values = []; + + foreach ($this->items as $item) { + $value = $item->unwrap(); + + if (null !== $value) { + $values[] = $value; + } + } + + return new self($values); + } + } + + /** @var ArrayList|Option> $list */ + $list = new ArrayList([]); + $numbers = $list->compact(); + ', + 'assertions' => [ + '$numbers' => 'ArrayList' + ], + ], ]; } @@ -221,6 +351,32 @@ public function freeze() ', 'error_message' => 'IfThisIsMismatch' ], + 'failWithInvalidTemplateConstraint' => [ + '> + * @return ArrayList + */ + public function compact(): ArrayList + { + throw new RuntimeException("???"); + } + } + + /** @var ArrayList $list */ + $list = new ArrayList(); + $numbers = $list->compact();', + 'error_message' => 'IfThisIsMismatch' + ], ]; } }