diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index 3b08e41b9..714ef936c 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -6,6 +6,7 @@ use Psalm\Codebase; use Psalm\CodeLocation; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\PhpVisitor\ParamReplacementVisitor; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\Comparator\TypeComparisonResult; use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Issue\ImplementedParamTypeMismatch; @@ -1068,7 +1069,8 @@ class MethodComparator $template_result = new \Psalm\Internal\Type\TemplateResult([], $template_types); - $templated_type->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $templated_type, $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index b09d79996..5990f9c51 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -8,6 +8,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ArrayFetchAnalyzer; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\VariableUseGraph; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Context; use Psalm\IssueBuffer; use Psalm\Issue\InvalidArrayAssignment; @@ -521,7 +522,8 @@ class ArrayAssignmentAnalyzer ] ); - $current_type->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $current_type, $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 1a922481a..4885ee454 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -18,6 +18,7 @@ use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\TemplateBound; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\UnionTemplateHandler; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\CodeLocation; use Psalm\Context; use Psalm\Issue\ImplicitToStringCast; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index d17d3abcc..bb1af673b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -17,6 +17,7 @@ use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\UnionTemplateHandler; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\CodeLocation; use Psalm\Context; use Psalm\Issue\InvalidPassByReference; @@ -347,7 +348,8 @@ class ArgumentsAnalyzer 'fn-' . ($context->calling_method_id ?: $context->calling_function_id) ); - $replaced_type->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $replaced_type, $replace_template_result, $codebase ); @@ -1001,7 +1003,8 @@ class ArgumentsAnalyzer ); if ($template_result->upper_bounds) { - $original_by_ref_type->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $original_by_ref_type, $template_result, $codebase ); @@ -1024,7 +1027,8 @@ class ArgumentsAnalyzer ); if ($template_result->upper_bounds) { - $original_by_ref_out_type->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $original_by_ref_out_type, $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index 2f76a0d84..f743d0253 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -11,6 +11,7 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\Type\TypeCombiner; use Psalm\Internal\Type\UnionTemplateHandler; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\CodeLocation; use Psalm\Context; @@ -841,7 +842,7 @@ class ArrayFunctionArgumentsAnalyzer $context->calling_method_id ?: $context->calling_function_id ); - $closure_type->return_type->replaceTemplateTypesWithArgTypes( + $closure_type->replaceTemplateTypesWithArgTypes( $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 8bb4006cb..bd249489f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -12,6 +12,7 @@ use Psalm\Internal\DataFlow\TaintSource; use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\Type\TypeExpander; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Storage\FunctionLikeStorage; use Psalm\Type; use Psalm\Type\Atomic\TCallable; @@ -104,7 +105,8 @@ class FunctionCallReturnTypeFetcher null ); - $return_type->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $return_type, $template_result, $codebase ); @@ -494,7 +496,8 @@ class FunctionCallReturnTypeFetcher foreach ($function_storage->conditionally_removed_taints as $conditionally_removed_taint) { $conditionally_removed_taint = clone $conditionally_removed_taint; - $conditionally_removed_taint->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $conditionally_removed_taint, $template_result, $codebase ); 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 00563dd90..8b28c6f01 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -11,6 +11,7 @@ use Psalm\Context; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\TemplateBound; use Psalm\Internal\Type\TemplateResult; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Type; use Psalm\Type\Atomic\TGenericObject; use function strtolower; @@ -461,7 +462,8 @@ class MethodCallReturnTypeFetcher null ); - $return_type_candidate->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $return_type_candidate, $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index 68465c72a..c1da4f927 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -9,6 +9,7 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\CodeLocation; use Psalm\Context; use Psalm\Internal\MethodIdentifier; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Issue\NonStaticSelfCall; use Psalm\Issue\ParentNotFound; use Psalm\IssueBuffer; @@ -280,7 +281,8 @@ class StaticCallAnalyzer extends CallAnalyzer foreach ($method_storage->conditionally_removed_taints as $conditionally_removed_taint) { $conditionally_removed_taint = clone $conditionally_removed_taint; - $conditionally_removed_taint->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $conditionally_removed_taint, $template_result, $codebase ); 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 b9c74d670..afac6c37a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -11,6 +11,7 @@ use Psalm\Context; use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\TemplateBound; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Issue\AbstractMethodCall; use Psalm\Issue\ImpureMethodCall; use Psalm\IssueBuffer; @@ -282,7 +283,8 @@ class ExistingAtomicStaticCallAnalyzer null ); - $return_type_candidate->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $return_type_candidate, $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 029523992..3e52f8c5a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -7,6 +7,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Internal\Codebase\TaintFlowGraph; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\CodeLocation; use Psalm\Context; use Psalm\Issue\EmptyArrayAccess; @@ -1329,7 +1330,8 @@ class ArrayFetchAnalyzer $expected_value_param_get = clone $type->value_param; - $expected_value_param_get->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $expected_value_param_get, $template_result_get, $codebase ); @@ -1337,7 +1339,8 @@ class ArrayFetchAnalyzer if ($replacement_type) { $expected_value_param_set = clone $type->value_param; - $replacement_type->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $replacement_type, $template_result_set, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index 12e378aa1..bef261490 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -11,6 +11,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier; use Psalm\Internal\Analyzer\Statements\Expression\Assignment\InstancePropertyAssignmentAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\CodeLocation; use Psalm\Context; use Psalm\Issue\DeprecatedProperty; @@ -751,7 +752,8 @@ class AtomicPropertyFetchAnalyzer } } - $class_property_type->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $class_property_type, new TemplateResult([], $template_types), $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index ffd7e6576..e433697e4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -17,6 +17,7 @@ use Psalm\Exception\DocblockParseException; use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\Type\TemplateResult; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Issue\FalsableReturnStatement; use Psalm\Issue\InvalidDocblock; use Psalm\Issue\InvalidReturnStatement; @@ -268,7 +269,8 @@ class ReturnAnalyzer $local_return_type = clone $local_return_type; - $local_return_type->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $local_return_type, new TemplateResult([], $found_generic_params), $codebase ); diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index a28a94a57..71ec2f483 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -737,7 +737,8 @@ class Populator $mapped_name = $parent_template_type_names[$i] ?? null; if ($mapped_name) { - $storage->template_extended_params[$implemented_interface_storage->name][$mapped_name] = $type; + $storage->template_extended_params[$implemented_interface_storage->name][$mapped_name] + = $type; } } diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index 1c10d016c..82ba64272 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -10,6 +10,7 @@ use Psalm\CodeLocation; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Fetch\VariableFetchAnalyzer; use Psalm\Internal\Analyzer\TraitAnalyzer; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\Comparator\AtomicTypeComparator; use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Issue\DocblockTypeContradiction; @@ -832,7 +833,8 @@ class AssertionReconciler extends \Psalm\Type\Reconciler ); if ($template_type_map) { - $new_param->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $new_param, new TemplateResult([], $template_type_map), $codebase ); @@ -880,7 +882,8 @@ class AssertionReconciler extends \Psalm\Type\Reconciler ); if ($template_type_map) { - $new_param->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $new_param, new TemplateResult([], $template_type_map), $codebase ); diff --git a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php index 713cf2309..f8ccbae27 100644 --- a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php @@ -50,7 +50,7 @@ class GenericTypeComparator if (!empty($class_storage->template_extended_params[$container_class])) { $input_type_part = new TGenericObject( $input_type_part->value, - array_values($class_storage->template_extended_params[$container_class]) + \array_values($class_storage->template_extended_params[$container_class]) ); } } diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php new file mode 100644 index 000000000..85589d89b --- /dev/null +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -0,0 +1,331 @@ +upper_bounds ?: []; + + foreach ($union->getAtomicTypes() as $key => $atomic_type) { + $atomic_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); + + if ($atomic_type instanceof Atomic\TTemplateParam) { + $template_type = null; + + $traversed_type = \Psalm\Internal\Type\UnionTemplateHandler::getRootTemplateType( + $inferred_upper_bounds, + $atomic_type->param_name, + $atomic_type->defining_class + ); + + if ($traversed_type) { + $template_type = $traversed_type; + + if (!$atomic_type->as->isMixed() && $template_type->isMixed()) { + $template_type = clone $atomic_type->as; + } else { + $template_type = clone $template_type; + } + + if ($atomic_type->extra_types) { + foreach ($template_type->getAtomicTypes() as $template_type_key => $atomic_template_type) { + if ($atomic_template_type instanceof TNamedObject + || $atomic_template_type instanceof TTemplateParam + || $atomic_template_type instanceof TIterable + || $atomic_template_type instanceof Atomic\TObjectWithProperties + ) { + $atomic_template_type->extra_types = array_merge( + $atomic_type->extra_types, + $atomic_template_type->extra_types ?: [] + ); + } elseif ($atomic_template_type instanceof Atomic\TObject) { + $first_atomic_type = array_shift($atomic_type->extra_types); + + if ($atomic_type->extra_types) { + $first_atomic_type->extra_types = $atomic_type->extra_types; + } + + $template_type->removeType($template_type_key); + $template_type->addType($first_atomic_type); + } + } + } + } elseif ($codebase) { + foreach ($inferred_upper_bounds as $template_type_map) { + foreach ($template_type_map as $template_class => $_) { + if (substr($template_class, 0, 3) === 'fn-') { + continue; + } + + try { + $classlike_storage = $codebase->classlike_storage_provider->get($template_class); + + if ($classlike_storage->template_extended_params) { + $defining_class = $atomic_type->defining_class; + + if (isset($classlike_storage->template_extended_params[$defining_class])) { + $param_map = $classlike_storage->template_extended_params[$defining_class]; + + if (isset($param_map[$key]) + && isset($inferred_upper_bounds[(string) $param_map[$key]][$template_class]) + ) { + $template_name = (string) $param_map[$key]; + + $template_type + = clone $inferred_upper_bounds[$template_name][$template_class]->type; + } + } + } + } catch (\InvalidArgumentException $e) { + } + } + } + } + + if ($template_type) { + $keys_to_unset[] = $key; + + foreach ($template_type->getAtomicTypes() as $template_type_part) { + if ($template_type_part instanceof Atomic\TMixed) { + $is_mixed = true; + } + + $new_types[$template_type_part->getKey()] = $template_type_part; + } + } + } elseif ($atomic_type instanceof Atomic\TTemplateParamClass) { + $template_type = isset($inferred_upper_bounds[$atomic_type->param_name][$atomic_type->defining_class]) + ? clone $inferred_upper_bounds[$atomic_type->param_name][$atomic_type->defining_class]->type + : null; + + $class_template_type = null; + + if ($template_type) { + foreach ($template_type->getAtomicTypes() as $template_type_part) { + if ($template_type_part instanceof Atomic\TMixed + || $template_type_part instanceof Atomic\TObject + ) { + $class_template_type = new Atomic\TClassString(); + } elseif ($template_type_part instanceof Atomic\TNamedObject) { + $class_template_type = new Atomic\TClassString( + $template_type_part->value, + $template_type_part + ); + } elseif ($template_type_part instanceof Atomic\TTemplateParam) { + $first_atomic_type = array_values($template_type_part->as->getAtomicTypes())[0]; + + $class_template_type = new Atomic\TTemplateParamClass( + $template_type_part->param_name, + $template_type_part->as->getId(), + $first_atomic_type instanceof TNamedObject ? $first_atomic_type : null, + $template_type_part->defining_class + ); + } + } + } + + if ($class_template_type) { + $keys_to_unset[] = $key; + $new_types[$class_template_type->getKey()] = $class_template_type; + } + } elseif ($atomic_type instanceof Atomic\TTemplateIndexedAccess) { + $keys_to_unset[] = $key; + + $template_type = null; + + if (isset($inferred_upper_bounds[$atomic_type->array_param_name][$atomic_type->defining_class]) + && !empty($inferred_upper_bounds[$atomic_type->offset_param_name]) + ) { + $array_template_type + = $inferred_upper_bounds[$atomic_type->array_param_name][$atomic_type->defining_class]->type; + $offset_template_type + = array_values( + $inferred_upper_bounds[$atomic_type->offset_param_name] + )[0]->type; + + if ($array_template_type->isSingle() + && $offset_template_type->isSingle() + && !$array_template_type->isMixed() + && !$offset_template_type->isMixed() + ) { + $array_template_type = array_values($array_template_type->getAtomicTypes())[0]; + $offset_template_type = array_values($offset_template_type->getAtomicTypes())[0]; + + if ($array_template_type instanceof Atomic\TKeyedArray + && ($offset_template_type instanceof Atomic\TLiteralString + || $offset_template_type instanceof Atomic\TLiteralInt) + && isset($array_template_type->properties[$offset_template_type->value]) + ) { + $template_type = clone $array_template_type->properties[$offset_template_type->value]; + } + } + } + + if ($template_type) { + foreach ($template_type->getAtomicTypes() as $template_type_part) { + if ($template_type_part instanceof Atomic\TMixed) { + $is_mixed = true; + } + + $new_types[$template_type_part->getKey()] = $template_type_part; + } + } else { + $new_types[$key] = new Atomic\TMixed(); + } + } elseif ($atomic_type instanceof Atomic\TConditional + && $codebase + ) { + $template_type = isset($inferred_upper_bounds[$atomic_type->param_name][$atomic_type->defining_class]) + ? clone $inferred_upper_bounds[$atomic_type->param_name][$atomic_type->defining_class]->type + : null; + + $class_template_type = null; + + $atomic_type = clone $atomic_type; + + if ($template_type) { + self::replaceTemplateTypesWithArgTypes( + $atomic_type->as_type, + $template_result, + $codebase + ); + + if ($atomic_type->as_type->isNullable() && $template_type->isVoid()) { + $template_type = Type::getNull(); + } + + if (UnionTypeComparator::isContainedBy( + $codebase, + $template_type, + $atomic_type->conditional_type + )) { + $class_template_type = clone $atomic_type->if_type; + self::replaceTemplateTypesWithArgTypes( + $class_template_type, + $template_result, + $codebase + ); + } elseif (UnionTypeComparator::isContainedBy( + $codebase, + $template_type, + $atomic_type->as_type + ) + && !UnionTypeComparator::isContainedBy( + $codebase, + $atomic_type->as_type, + $template_type + ) + ) { + $class_template_type = clone $atomic_type->else_type; + self::replaceTemplateTypesWithArgTypes( + $class_template_type, + $template_result, + $codebase + ); + } + } + + if (!$class_template_type) { + self::replaceTemplateTypesWithArgTypes( + $atomic_type->if_type, + $template_result, + $codebase + ); + + self::replaceTemplateTypesWithArgTypes( + $atomic_type->else_type, + $template_result, + $codebase + ); + + $class_template_type = Type::combineUnionTypes( + $atomic_type->if_type, + $atomic_type->else_type, + $codebase + ); + } + + $keys_to_unset[] = $key; + + foreach ($class_template_type->getAtomicTypes() as $class_template_atomic_type) { + $new_types[$class_template_atomic_type->getKey()] = $class_template_atomic_type; + } + } + } + + $union->bustCache(); + + if ($is_mixed) { + if (!$new_types) { + throw new \UnexpectedValueException('This array should be full'); + } + + $union->replaceTypes($new_types); + + return; + } + + foreach ($keys_to_unset as $key) { + $union->removeType($key); + } + + $atomic_types = array_values(array_merge($union->getAtomicTypes(), $new_types)); + + $union->replaceTypes( + TypeCombiner::combine( + $atomic_types, + $codebase + )->getAtomicTypes() + ); + } +} diff --git a/src/Psalm/Internal/Type/UnionTemplateHandler.php b/src/Psalm/Internal/Type/UnionTemplateHandler.php index 0151af302..e9b3c64f7 100644 --- a/src/Psalm/Internal/Type/UnionTemplateHandler.php +++ b/src/Psalm/Internal/Type/UnionTemplateHandler.php @@ -8,6 +8,7 @@ use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Type\Union; use Psalm\Type\Atomic; use Psalm\Internal\Type\Comparator\CallableTypeComparator; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use function array_merge; use function array_values; use function count; @@ -17,6 +18,9 @@ use function substr; class UnionTemplateHandler { + /** + * This replaces template types in unions with standins (normally the template as type) + */ public static function replaceTemplateTypesWithStandins( Union $union_type, TemplateResult $template_result, @@ -974,7 +978,9 @@ class UnionTemplateHandler } $new_input_param = clone $new_input_param; - $new_input_param->replaceTemplateTypesWithArgTypes( + + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $new_input_param, new TemplateResult([], $replacement_templates), $codebase ); diff --git a/src/Psalm/Type/Atomic/CallableTrait.php b/src/Psalm/Type/Atomic/CallableTrait.php index 36bf87ff4..0b50eb24e 100644 --- a/src/Psalm/Type/Atomic/CallableTrait.php +++ b/src/Psalm/Type/Atomic/CallableTrait.php @@ -8,6 +8,7 @@ use Psalm\Codebase; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\UnionTemplateHandler; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Storage\FunctionLikeParameter; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -252,12 +253,20 @@ trait CallableTrait continue; } - $param->type->replaceTemplateTypesWithArgTypes($template_result, $codebase); + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $param->type, + $template_result, + $codebase + ); } } if ($this->return_type) { - $this->return_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $this->return_type, + $template_result, + $codebase + ); } } diff --git a/src/Psalm/Type/Atomic/GenericTrait.php b/src/Psalm/Type/Atomic/GenericTrait.php index f2e48849c..f8aae8021 100644 --- a/src/Psalm/Type/Atomic/GenericTrait.php +++ b/src/Psalm/Type/Atomic/GenericTrait.php @@ -7,6 +7,7 @@ use Psalm\Codebase; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\UnionTemplateHandler; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -224,7 +225,11 @@ trait GenericTrait ?Codebase $codebase ) : void { foreach ($this->type_params as $offset => $type_param) { - $type_param->replaceTemplateTypesWithArgTypes($template_result, $codebase); + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $type_param, + $template_result, + $codebase + ); if ($this instanceof Atomic\TArray && $offset === 0 && $type_param->isMixed()) { $this->type_params[0] = \Psalm\Type::getArrayKey(); diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index 2bf3ac6c8..a753cf79e 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -6,6 +6,7 @@ use Psalm\Codebase; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\UnionTemplateHandler; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -196,7 +197,11 @@ class TClassStringMap extends \Psalm\Type\Atomic TemplateResult $template_result, ?Codebase $codebase ) : void { - $this->value_param->replaceTemplateTypesWithArgTypes($template_result, $codebase); + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $this->value_param, + $template_result, + $codebase + ); } public function getChildNodes() : array diff --git a/src/Psalm/Type/Atomic/TConditional.php b/src/Psalm/Type/Atomic/TConditional.php index c08ebe3e4..5deb3860b 100644 --- a/src/Psalm/Type/Atomic/TConditional.php +++ b/src/Psalm/Type/Atomic/TConditional.php @@ -3,6 +3,7 @@ namespace Psalm\Type\Atomic; use Psalm\Codebase; use Psalm\Internal\Type\TemplateResult; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Type\Union; class TConditional extends \Psalm\Type\Atomic @@ -133,6 +134,10 @@ class TConditional extends \Psalm\Type\Atomic TemplateResult $template_result, ?Codebase $codebase ) : void { - $this->conditional_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $this->conditional_type, + $template_result, + $codebase + ); } } diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index eb1378f7d..9ff467124 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -13,6 +13,7 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TypeCombiner; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\UnionTemplateHandler; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -352,7 +353,8 @@ class TKeyedArray extends \Psalm\Type\Atomic ?Codebase $codebase ) : void { foreach ($this->properties as $property) { - $property->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $property, $template_result, $codebase ); diff --git a/src/Psalm/Type/Atomic/TList.php b/src/Psalm/Type/Atomic/TList.php index e3d9820ce..0cf4ef01d 100644 --- a/src/Psalm/Type/Atomic/TList.php +++ b/src/Psalm/Type/Atomic/TList.php @@ -6,6 +6,7 @@ use Psalm\Codebase; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\UnionTemplateHandler; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -166,7 +167,11 @@ class TList extends \Psalm\Type\Atomic TemplateResult $template_result, ?Codebase $codebase ) : void { - $this->type_param->replaceTemplateTypesWithArgTypes($template_result, $codebase); + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $this->type_param, + $template_result, + $codebase + ); } public function equals(Atomic $other_type): bool diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index b9a1b3621..06df9158c 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -11,6 +11,7 @@ use Psalm\Type\Union; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\UnionTemplateHandler; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; use function array_merge; use function array_values; @@ -258,7 +259,8 @@ class TObjectWithProperties extends TObject ?Codebase $codebase ) : void { foreach ($this->properties as $property) { - $property->replaceTemplateTypesWithArgTypes( + TemplateInferredTypeReplacer::replaceTemplateTypesWithArgTypes( + $property, $template_result, $codebase ); diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 65dc09555..31ebcb346 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -223,13 +223,11 @@ class Union implements TypeNode } /** - * @return array - * @deprecated in favour of getAtomicTypes() - * @psalm-suppress PossiblyUnusedMethod + * @param non-empty-array $types */ - public function getTypes(): array + public function replaceTypes(array $types) : void { - return $this->types; + $this->types = $types; } /** @@ -1041,298 +1039,6 @@ class Union implements TypeNode $this->id = null; } - public function replaceTemplateTypesWithArgTypes( - TemplateResult $template_result, - ?Codebase $codebase - ) : void { - $keys_to_unset = []; - - $new_types = []; - - $is_mixed = false; - - $inferred_upper_bounds = $template_result->upper_bounds ?: []; - - foreach ($this->types as $key => $atomic_type) { - $atomic_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); - - if ($atomic_type instanceof Type\Atomic\TTemplateParam) { - $template_type = null; - - $traversed_type = \Psalm\Internal\Type\UnionTemplateHandler::getRootTemplateType( - $inferred_upper_bounds, - $atomic_type->param_name, - $atomic_type->defining_class - ); - - if ($traversed_type) { - $template_type = $traversed_type; - - if (!$atomic_type->as->isMixed() && $template_type->isMixed()) { - $template_type = clone $atomic_type->as; - } else { - $template_type = clone $template_type; - } - - if ($atomic_type->extra_types) { - foreach ($template_type->getAtomicTypes() as $template_type_key => $atomic_template_type) { - if ($atomic_template_type instanceof TNamedObject - || $atomic_template_type instanceof TTemplateParam - || $atomic_template_type instanceof TIterable - || $atomic_template_type instanceof Type\Atomic\TObjectWithProperties - ) { - $atomic_template_type->extra_types = array_merge( - $atomic_type->extra_types, - $atomic_template_type->extra_types ?: [] - ); - } elseif ($atomic_template_type instanceof Type\Atomic\TObject) { - $first_atomic_type = array_shift($atomic_type->extra_types); - - if ($atomic_type->extra_types) { - $first_atomic_type->extra_types = $atomic_type->extra_types; - } - - $template_type->removeType($template_type_key); - $template_type->addType($first_atomic_type); - } - } - } - } elseif ($codebase) { - foreach ($inferred_upper_bounds as $template_type_map) { - foreach ($template_type_map as $template_class => $_) { - if (substr($template_class, 0, 3) === 'fn-') { - continue; - } - - try { - $classlike_storage = $codebase->classlike_storage_provider->get($template_class); - - if ($classlike_storage->template_extended_params) { - $defining_class = $atomic_type->defining_class; - - if (isset($classlike_storage->template_extended_params[$defining_class])) { - $param_map = $classlike_storage->template_extended_params[$defining_class]; - - if (isset($param_map[$key]) - && isset($inferred_upper_bounds[(string) $param_map[$key]][$template_class]) - ) { - $template_name = (string) $param_map[$key]; - - $template_type - = clone $inferred_upper_bounds[$template_name][$template_class]->type; - } - } - } - } catch (\InvalidArgumentException $e) { - } - } - } - } - - if ($template_type) { - $keys_to_unset[] = $key; - - foreach ($template_type->types as $template_type_part) { - if ($template_type_part instanceof Type\Atomic\TMixed) { - $is_mixed = true; - } - - $new_types[$template_type_part->getKey()] = $template_type_part; - } - } - } elseif ($atomic_type instanceof Type\Atomic\TTemplateParamClass) { - $template_type = isset($inferred_upper_bounds[$atomic_type->param_name][$atomic_type->defining_class]) - ? clone $inferred_upper_bounds[$atomic_type->param_name][$atomic_type->defining_class]->type - : null; - - $class_template_type = null; - - if ($template_type) { - foreach ($template_type->types as $template_type_part) { - if ($template_type_part instanceof Type\Atomic\TMixed - || $template_type_part instanceof Type\Atomic\TObject - ) { - $class_template_type = new Type\Atomic\TClassString(); - } elseif ($template_type_part instanceof Type\Atomic\TNamedObject) { - $class_template_type = new Type\Atomic\TClassString( - $template_type_part->value, - $template_type_part - ); - } elseif ($template_type_part instanceof Type\Atomic\TTemplateParam) { - $first_atomic_type = array_values($template_type_part->as->types)[0]; - - $class_template_type = new Type\Atomic\TTemplateParamClass( - $template_type_part->param_name, - $template_type_part->as->getId(), - $first_atomic_type instanceof TNamedObject ? $first_atomic_type : null, - $template_type_part->defining_class - ); - } - } - } - - if ($class_template_type) { - $keys_to_unset[] = $key; - $new_types[$class_template_type->getKey()] = $class_template_type; - } - } elseif ($atomic_type instanceof Type\Atomic\TTemplateIndexedAccess) { - $keys_to_unset[] = $key; - - $template_type = null; - - if (isset($inferred_upper_bounds[$atomic_type->array_param_name][$atomic_type->defining_class]) - && !empty($inferred_upper_bounds[$atomic_type->offset_param_name]) - ) { - $array_template_type - = $inferred_upper_bounds[$atomic_type->array_param_name][$atomic_type->defining_class]->type; - $offset_template_type - = array_values( - $inferred_upper_bounds[$atomic_type->offset_param_name] - )[0]->type; - - if ($array_template_type->isSingle() - && $offset_template_type->isSingle() - && !$array_template_type->isMixed() - && !$offset_template_type->isMixed() - ) { - $array_template_type = array_values($array_template_type->types)[0]; - $offset_template_type = array_values($offset_template_type->types)[0]; - - if ($array_template_type instanceof Type\Atomic\TKeyedArray - && ($offset_template_type instanceof Type\Atomic\TLiteralString - || $offset_template_type instanceof Type\Atomic\TLiteralInt) - && isset($array_template_type->properties[$offset_template_type->value]) - ) { - $template_type = clone $array_template_type->properties[$offset_template_type->value]; - } - } - } - - if ($template_type) { - foreach ($template_type->types as $template_type_part) { - if ($template_type_part instanceof Type\Atomic\TMixed) { - $is_mixed = true; - } - - $new_types[$template_type_part->getKey()] = $template_type_part; - } - } else { - $new_types[$key] = new Type\Atomic\TMixed(); - } - } elseif ($atomic_type instanceof Type\Atomic\TConditional - && $codebase - ) { - $template_type = isset($inferred_upper_bounds[$atomic_type->param_name][$atomic_type->defining_class]) - ? clone $inferred_upper_bounds[$atomic_type->param_name][$atomic_type->defining_class]->type - : null; - - $class_template_type = null; - - $atomic_type = clone $atomic_type; - - if ($template_type) { - $atomic_type->as_type->replaceTemplateTypesWithArgTypes( - $template_result, - $codebase - ); - - if ($atomic_type->as_type->isNullable() && $template_type->isVoid()) { - $template_type = Type::getNull(); - } - - if (UnionTypeComparator::isContainedBy( - $codebase, - $template_type, - $atomic_type->conditional_type - )) { - $class_template_type = clone $atomic_type->if_type; - $class_template_type->replaceTemplateTypesWithArgTypes( - $template_result, - $codebase - ); - } elseif (UnionTypeComparator::isContainedBy( - $codebase, - $template_type, - $atomic_type->as_type - ) - && !UnionTypeComparator::isContainedBy( - $codebase, - $atomic_type->as_type, - $template_type - ) - ) { - $class_template_type = clone $atomic_type->else_type; - $class_template_type->replaceTemplateTypesWithArgTypes( - $template_result, - $codebase - ); - } - } - - if (!$class_template_type) { - $atomic_type->if_type->replaceTemplateTypesWithArgTypes( - $template_result, - $codebase - ); - - $atomic_type->else_type->replaceTemplateTypesWithArgTypes( - $template_result, - $codebase - ); - - $class_template_type = Type::combineUnionTypes( - $atomic_type->if_type, - $atomic_type->else_type, - $codebase - ); - } - - $keys_to_unset[] = $key; - - foreach ($class_template_type->getAtomicTypes() as $class_template_atomic_type) { - $new_types[$class_template_atomic_type->getKey()] = $class_template_atomic_type; - } - } - } - - $this->id = null; - - if ($is_mixed) { - if (!$new_types) { - throw new \UnexpectedValueException('This array should be full'); - } - - $this->types = $new_types; - - return; - } - - foreach ($keys_to_unset as $key) { - unset($this->types[$key]); - } - - foreach ($new_types as $type) { - if ($type instanceof TLiteralString) { - $this->literal_string_types[$type->getKey()] = $type; - } elseif ($type instanceof TLiteralInt) { - $this->literal_int_types[$type->getKey()] = $type; - } elseif ($type instanceof TLiteralFloat) { - $this->literal_float_types[$type->getKey()] = $type; - } - } - - $atomic_types = array_values(array_merge($this->types, $new_types)); - - if ($atomic_types) { - $this->types = TypeCombiner::combine( - $atomic_types, - $codebase - )->getAtomicTypes(); - - $this->id = null; - } - } - public function isSingle(): bool { $type_count = count($this->types);