diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/CreateTemplateResult.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/CreateTemplateResult.php new file mode 100644 index 00000000000..8134c299e72 --- /dev/null +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/CreateTemplateResult.php @@ -0,0 +1,218 @@ +template_types ?? [], + lower_bounds: [], + ); + } + + public static function forMethod( + CallLike $stmt, + Context $context, + Codebase $codebase, + StatementsAnalyzer $statements_analyzer, + TNamedObject $lhs_type_part, + ?MethodStorage $method_storage, + ClassLikeStorage $class_storage, + ): CollectedArgumentTemplates { + if ($method_storage === null || $method_storage->defining_fqcln === null) { + return new CollectedArgumentTemplates(); + } + + if ($lhs_type_part instanceof TClosure || $method_storage->cased_name === null) { + return new CollectedArgumentTemplates(); + } + + $method_name_lc = strtolower($method_storage->cased_name); + $self_call = !$statements_analyzer->isStatic() && $class_storage->name === $context->self; + + if ($self_call) { + $trait_lower_bounds = self::getTraitLowerBounds( + static_class_storage: $class_storage, + statements_analyzer: $statements_analyzer, + lhs_type_part: $lhs_type_part, + method_name_lc: $method_name_lc, + ); + + if ($trait_lower_bounds !== null) { + return new CollectedArgumentTemplates( + template_types: array_merge( + $class_storage->template_types ?? [], + $method_storage->template_types ?? [], + ), + lower_bounds: $trait_lower_bounds, + ); + } + } + + $method_id = new MethodIdentifier(strtolower($method_storage->defining_fqcln), $method_name_lc); + $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id) ?? $method_id; + + $class_lower_bounds = ClassTemplateParamCollector::collect( + codebase: $codebase, + class_storage: $codebase->methods->getClassLikeStorageForMethod($declaring_method_id), + static_class_storage: $class_storage, + method_name: $method_name_lc, + lhs_type_part: $lhs_type_part, + self_call: $self_call, + ) ?? []; + + return new CollectedArgumentTemplates( + template_types: array_merge( + $class_storage->template_types ?? [], + $method_storage->template_types ?? [], + ), + lower_bounds: array_merge( + self::withParent($codebase, $context, $stmt, $class_lower_bounds), + self::getIfThisIsTypeLowerBounds($statements_analyzer, $method_storage, $lhs_type_part), + ), + ); + } + + /** + * @param array> $lower_bounds + * @return array> + */ + private static function withParent(Codebase $codebase, Context $context, CallLike $stmt, array $lower_bounds): array + { + $parent_call = $stmt instanceof StaticCall + && $stmt->class instanceof Name + && $stmt->class->getParts() === ['parent']; + + $template_extended_params = $parent_call && $context->self !== null + ? $codebase->classlike_storage_provider->get($context->self)->template_extended_params ?? [] + : []; + + foreach ($template_extended_params as $template_fq_class_name => $extended_types) { + foreach ($extended_types as $type_key => $extended_type) { + if (isset($lower_bounds[$type_key][$template_fq_class_name])) { + $lower_bounds[$type_key][$template_fq_class_name] = $extended_type; + continue; + } + + foreach ($extended_type->getAtomicTypes() as $t) { + $lower_bounds[$type_key][$template_fq_class_name] = + $t instanceof TTemplateParam && isset($lower_bounds[$t->param_name][$t->defining_class]) + ? $lower_bounds[$t->param_name][$t->defining_class] + : $extended_type; + } + } + } + + return $lower_bounds; + } + + /** + * @param lowercase-string $method_name_lc + * @return array> + */ + private static function getTraitLowerBounds( + ClassLikeStorage $static_class_storage, + StatementsAnalyzer $statements_analyzer, + TNamedObject $lhs_type_part, + string $method_name_lc, + ): ?array { + $parent_source = $statements_analyzer->getSource(); + + if (!$parent_source instanceof FunctionLikeAnalyzer) { + return null; + } + + $grandparent_source = $parent_source->getSource(); + + if (!$grandparent_source instanceof TraitAnalyzer) { + return null; + } + + $fq_trait_name_lc = strtolower($grandparent_source->getFQCLN()); + + $codebase = $statements_analyzer->getCodebase(); + $trait_storage = $codebase->classlike_storage_provider->get($fq_trait_name_lc); + + if (!isset($trait_storage->methods[$method_name_lc])) { + return null; + } + + $trait_method_id = new MethodIdentifier($trait_storage->name, $method_name_lc); + + return ClassTemplateParamCollector::collect( + codebase: $codebase, + class_storage: $codebase->methods->getClassLikeStorageForMethod($trait_method_id), + static_class_storage: $static_class_storage, + method_name: $method_name_lc, + lhs_type_part: $lhs_type_part, + self_call: true, + ) ?? []; + } + + /** + * @return array> + */ + private static function getIfThisIsTypeLowerBounds( + StatementsAnalyzer $statements_analyzer, + MethodStorage $method_storage, + TNamedObject $lhs_type_part, + ): array { + if ($method_storage->if_this_is_type === null) { + return []; + } + + $codebase = $statements_analyzer->getCodebase(); + $method_template_result = new TemplateResult($method_storage->template_types ?? [], []); + + TemplateStandinTypeReplacer::fillTemplateResult( + $method_storage->if_this_is_type, + $method_template_result, + $codebase, + $statements_analyzer, + new Union([$lhs_type_part]), + ); + + return array_map( + static fn(array $template_map): array => array_map( + static fn(array $lower_bounds): Union => TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( + $lower_bounds, + $codebase, + ), + $template_map, + ), + $method_template_result->lower_bounds, + ); + } +} 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 30f32e89ed8..29c03622056 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -13,6 +13,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentMapPopulator; use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\ArgumentsTemplateResultCollector; use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\CallLikeContextualTypeExtractor; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\CreateTemplateResult; use Psalm\Internal\Analyzer\Statements\Expression\Call\FunctionCallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier; @@ -172,12 +173,14 @@ public static function analyze( $method_storage = null; } - $collected_argument_templates = ArgumentsTemplateResultCollector::collect( + $collected_argument_templates = CreateTemplateResult::forMethod( $stmt, $context, + $codebase, $statements_analyzer, - $method_storage, $lhs_type_part, + $method_storage, + $class_storage, ); $template_result = new TemplateResult(