From ef60a0c88cb28eab70bff448ab16e2153f1e7524 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 3 Oct 2022 11:28:01 +0200 Subject: [PATCH] Fix properties-of on generics&intersections --- psalm-baseline.xml | 13 +- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 2 +- .../Analyzer/FunctionLikeAnalyzer.php | 2 +- .../Analyzer/Statements/Block/TryAnalyzer.php | 2 +- .../Expression/Call/ArgumentsAnalyzer.php | 38 +++-- .../Call/ArrayFunctionArgumentsAnalyzer.php | 18 ++- .../Call/ClassTemplateParamCollector.php | 3 +- .../Call/Method/AtomicMethodCallAnalyzer.php | 4 +- .../ExistingAtomicMethodCallAnalyzer.php | 2 +- src/Psalm/Internal/Codebase/Methods.php | 15 +- .../Internal/Type/AssertionReconciler.php | 9 +- .../Type/Comparator/AtomicTypeComparator.php | 4 +- .../Comparator/CallableTypeComparator.php | 2 +- .../Type/Comparator/ObjectComparator.php | 2 +- .../Type/TemplateInferredTypeReplacer.php | 62 ++++----- .../Type/TemplateStandinTypeReplacer.php | 34 ++++- src/Psalm/Internal/Type/TypeCombination.php | 4 +- src/Psalm/Internal/Type/TypeCombiner.php | 2 +- src/Psalm/Internal/Type/TypeExpander.php | 89 ++++++------ src/Psalm/Internal/Type/TypeParser.php | 7 +- src/Psalm/Storage/FunctionLikeParameter.php | 10 ++ src/Psalm/Type.php | 8 +- src/Psalm/Type/Atomic.php | 108 +++++---------- src/Psalm/Type/Atomic/CallableTrait.php | 114 ++++++++++----- src/Psalm/Type/Atomic/GenericTrait.php | 71 +++++++--- .../Type/Atomic/HasIntersectionTrait.php | 101 ++++++++++++-- .../Type/Atomic/TAnonymousClassInstance.php | 11 +- src/Psalm/Type/Atomic/TArray.php | 80 ++++++++++- src/Psalm/Type/Atomic/TCallable.php | 78 +++++++++++ src/Psalm/Type/Atomic/TCallableString.php | 1 + src/Psalm/Type/Atomic/TClassConstant.php | 16 +++ src/Psalm/Type/Atomic/TClassString.php | 52 +++++-- src/Psalm/Type/Atomic/TClassStringMap.php | 33 +++-- src/Psalm/Type/Atomic/TClosure.php | 131 ++++++++++++++++++ src/Psalm/Type/Atomic/TConditional.php | 18 ++- src/Psalm/Type/Atomic/TDependentGetClass.php | 3 +- .../Type/Atomic/TDependentGetDebugType.php | 4 +- src/Psalm/Type/Atomic/TDependentListKey.php | 4 +- src/Psalm/Type/Atomic/TGenericObject.php | 120 +++++++++++++++- src/Psalm/Type/Atomic/TIterable.php | 108 +++++++++++++-- src/Psalm/Type/Atomic/TKeyedArray.php | 73 ++++++++-- src/Psalm/Type/Atomic/TList.php | 39 ++++-- src/Psalm/Type/Atomic/TLiteralClassString.php | 11 ++ src/Psalm/Type/Atomic/TLowercaseString.php | 2 + src/Psalm/Type/Atomic/TMixed.php | 1 + src/Psalm/Type/Atomic/TNamedObject.php | 79 ++++++++++- src/Psalm/Type/Atomic/TNonEmptyArray.php | 19 +++ src/Psalm/Type/Atomic/TNonEmptyList.php | 15 ++ .../Type/Atomic/TObjectWithProperties.php | 69 ++++++++- src/Psalm/Type/Atomic/TPropertiesOf.php | 38 +++-- .../Type/Atomic/TTemplateIndexedAccess.php | 2 + src/Psalm/Type/Atomic/TTemplateKeyOf.php | 15 +- src/Psalm/Type/Atomic/TTemplateParam.php | 60 +++++++- .../Type/Atomic/TTemplatePropertiesOf.php | 28 +++- src/Psalm/Type/Atomic/TTemplateValueOf.php | 15 +- src/Psalm/Type/Atomic/TTypeAlias.php | 2 + src/Psalm/Type/MutableUnion.php | 38 ++++- src/Psalm/Type/Union.php | 9 ++ .../Codebase/InternalCallMapHandlerTest.php | 5 +- tests/PropertiesOfTest.php | 38 +++++ tests/Template/ClassTemplateTest.php | 43 ++++++ tests/Template/PropertiesOfTemplateTest.php | 31 +++++ 62 files changed, 1622 insertions(+), 395 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index e140ba1a087..09f1f3a3587 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + $comment_block->tags['variablesfrom'][0] @@ -331,9 +331,16 @@ $this->type_params[1] + + replaceTypeParams + replaceTypeParams + replaceTypeParams + - - + + + replaceAs + diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index d585d398584..f8148f1c183 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -758,7 +758,7 @@ public static function addContextProperties( // Get actual types used for templates (to support @template-covariant) $template_standins = new TemplateResult($lower_bounds, []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( $guide_property_type, $template_standins, $codebase, diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 735dc7fef0a..b45ef64e430 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -1837,7 +1837,7 @@ private function getFunctionInformation( if ($this->storage instanceof MethodStorage && $this->storage->if_this_is_type) { $template_result = new TemplateResult($this->getTemplateTypeMap() ?? [], []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( new Union([$this_object_type]), $template_result, $codebase, diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php index 1160f615394..68fc8398a9b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php @@ -273,7 +273,7 @@ static function (string $fq_catch_class) use ($codebase): TNamedObject { && $codebase->interfaceExists($fq_catch_class) && !$codebase->interfaceExtends($fq_catch_class, 'Throwable') ) { - $catch_class_type->addIntersectionType(new TNamedObject('Throwable')); + return $catch_class_type->addIntersectionType(new TNamedObject('Throwable')); } return $catch_class_type; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 424c748f36b..533cda89821 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -268,7 +268,7 @@ public static function analyze( if (null !== $inferred_arg_type && null !== $template_result && null !== $param && null !== $param->type) { $codebase = $statements_analyzer->getCodebase(); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $param->type, $template_result, $codebase, @@ -308,19 +308,6 @@ private static function handleArrayMapFilterArrayArg( ): void { $codebase = $statements_analyzer->getCodebase(); - $generic_param_type = new Union([ - new TArray([ - Type::getArrayKey(), - new Union([ - new TTemplateParam( - 'ArrayValue' . $argument_offset, - Type::getMixed(), - $method_id - ) - ]) - ]) - ]); - $template_types = ['ArrayValue' . $argument_offset => [$method_id => Type::getMixed()]]; $replace_template_result = new TemplateResult( @@ -330,8 +317,19 @@ private static function handleArrayMapFilterArrayArg( $existing_type = $statements_analyzer->node_data->getType($arg->value); - TemplateStandinTypeReplacer::replace( - $generic_param_type, + TemplateStandinTypeReplacer::fillTemplateResult( + new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TTemplateParam( + 'ArrayValue' . $argument_offset, + Type::getMixed(), + $method_id + ) + ]) + ]) + ]), $replace_template_result, $codebase, $statements_analyzer, @@ -515,7 +513,7 @@ private static function handleHighOrderFuncCallArg( $actual_func_param->type->getTemplateTypes() && isset($container_hof_atomic->params[$offset]) ) { - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $actual_func_param->type, $high_order_template_result, $codebase, @@ -769,7 +767,7 @@ public static function checkArgumentsMatch( } } - if ($function_params) { + if ($function_params && !$is_variadic) { foreach ($function_params as $function_param) { $is_variadic = $is_variadic || $function_param->is_variadic; } @@ -1616,7 +1614,7 @@ private static function getProvisionalTemplateResultForFunctionLike( $calling_class_storage->final ?? false ); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( $fleshed_out_param_type, $template_result, $codebase, @@ -1796,7 +1794,7 @@ private static function checkArgCount( $default_type = new Union([$default_type_atomic]); } - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $param->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 f3852df5154..88b6df7a3a6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -115,6 +115,7 @@ public static function checkArgumentsMatch( $max_closure_param_count = count($args) > 2 ? 2 : 1; } + $new = []; foreach ($closure_arg_type->getAtomicTypes() as $closure_type) { self::checkClosureType( $statements_analyzer, @@ -127,7 +128,13 @@ public static function checkArgumentsMatch( $array_arg_types, $check_functions ); + $new []= $closure_type; } + + $statements_analyzer->node_data->setType( + $closure_arg->value, + $closure_arg_type->getBuilder()->setTypes($new)->freeze() + ); } } @@ -584,13 +591,12 @@ public static function handleByRefArrayAdjustment( /** * @param (TArray|null)[] $array_arg_types - * */ private static function checkClosureType( StatementsAnalyzer $statements_analyzer, Context $context, string $method_id, - Atomic $closure_type, + Atomic &$closure_type, PhpParser\Node\Arg $closure_arg, int $min_closure_param_count, int $max_closure_param_count, @@ -726,10 +732,10 @@ private static function checkClosureType( } } } else { - $closure_types = [$closure_type]; + $closure_types = [&$closure_type]; } - foreach ($closure_types as $closure_type) { + foreach ($closure_types as &$closure_type) { if ($closure_type->params === null) { continue; } @@ -755,7 +761,7 @@ private static function checkClosureTypeArgs( StatementsAnalyzer $statements_analyzer, Context $context, string $method_id, - Atomic $closure_type, + Atomic &$closure_type, PhpParser\Node\Arg $closure_arg, int $min_closure_param_count, int $max_closure_param_count, @@ -863,7 +869,7 @@ private static function checkClosureTypeArgs( $context->calling_method_id ?: $context->calling_function_id ); - $closure_type->replaceTemplateTypesWithArgTypes( + $closure_type = $closure_type->replaceTemplateTypesWithArgTypes( $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php index d498beff791..30022eb2a82 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php @@ -240,8 +240,7 @@ private static function resolveTemplateParam( } } else { if ($template_result !== null) { - $type_extends_atomic = clone $type_extends_atomic; - $type_extends_atomic->replaceTemplateTypesWithArgTypes( + $type_extends_atomic = $type_extends_atomic->replaceTemplateTypesWithArgTypes( $template_result, $codebase ); 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 1c9edaf7607..9d6401c66c0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -849,9 +849,7 @@ private static function handleRegularMixins( $lhs_var_id === '$this' ); - $lhs_type_part = clone $mixin; - - $lhs_type_part->replaceTemplateTypesWithArgTypes( + $lhs_type_part = $mixin->replaceTemplateTypesWithArgTypes( new TemplateResult([], $mixin_class_template_params ?: []), $codebase ); 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 72c0c7cbda5..795b6b1c02c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -207,7 +207,7 @@ public static function analyze( if ($method_storage && $method_storage->if_this_is_type) { $method_template_result = new TemplateResult($method_storage->template_types ?: [], []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $method_storage->if_this_is_type, $method_template_result, $codebase, diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index da7f59b6122..262101a74f1 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -887,12 +887,17 @@ public function getMethodReturnType( if ((!$old_contained_by_new && !$new_contained_by_old) || ($old_contained_by_new && $new_contained_by_old) ) { + $attempted_intersection = null; if ($old_contained_by_new) { //implicitly $new_contained_by_old as well - $attempted_intersection = Type::intersectUnionTypes( - $candidate_type, - $overridden_storage->return_type, - $source_analyzer->getCodebase() - ); + try { + $attempted_intersection = Type::intersectUnionTypes( + $candidate_type, + $overridden_storage->return_type, + $source_analyzer->getCodebase() + ); + } catch (InvalidArgumentException $e) { + // TODO: fix + } } else { $attempted_intersection = Type::intersectUnionTypes( $overridden_storage->return_type, diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index ee5c5667fc1..e62b04d849f 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -402,14 +402,12 @@ private static function refine( && ($codebase->classExists($existing_var_type_part->value) || $codebase->interfaceExists($existing_var_type_part->value)) ) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->addIntersectionType($new_type_part); + $existing_var_type_part = $existing_var_type_part->addIntersectionType($new_type_part); $acceptable_atomic_types[] = $existing_var_type_part; } if ($existing_var_type_part instanceof TTemplateParam) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->addIntersectionType($new_type_part); + $existing_var_type_part = $existing_var_type_part->addIntersectionType($new_type_part); $acceptable_atomic_types[] = $existing_var_type_part; } } @@ -1617,8 +1615,7 @@ private static function handleIsA( if ($codebase->classExists($existing_var_type_part->value) || $codebase->interfaceExists($existing_var_type_part->value) ) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->addIntersectionType($new_type_part); + $existing_var_type_part = $existing_var_type_part->addIntersectionType($new_type_part); $acceptable_atomic_types[] = $existing_var_type_part; } } diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index 28064420305..870026ff6fa 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -63,10 +63,10 @@ public static function isContainedBy( if (($container_type_part instanceof TTemplateParam || ($container_type_part instanceof TNamedObject - && isset($container_type_part->extra_types))) + && $container_type_part->extra_types)) && ($input_type_part instanceof TTemplateParam || ($input_type_part instanceof TNamedObject - && isset($input_type_part->extra_types))) + && $input_type_part->extra_types)) ) { return ObjectComparator::isShallowlyContainedBy( $codebase, diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 2670ff04d87..d3a93bd60eb 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -409,7 +409,7 @@ public static function getCallableFromAtomic( $input_with_templates = new Atomic\TGenericObject($input_type_part->value, $type_params); $template_result = new TemplateResult($invokable_storage->template_types ?? [], []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( new Type\Union([$input_with_templates]), $template_result, $codebase, diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index 5e3aff80828..a2423abf3b3 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -115,7 +115,7 @@ private static function getIntersectionTypes(Atomic $type_part): array $type_part = clone $type_part; $extra_types = $type_part->extra_types; - $type_part->extra_types = null; + $type_part->extra_types = []; $extra_types[$type_part->getKey()] = $type_part; diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index 0b0664f989b..d293baecdd6 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -48,17 +48,17 @@ public static function replace( TemplateResult $template_result, ?Codebase $codebase ): Union { - $keys_to_unset = []; - $new_types = []; $is_mixed = false; $inferred_lower_bounds = $template_result->lower_bounds ?: []; - $union = $union->getBuilder(); + $types = []; + foreach ($union->getAtomicTypes() as $key => $atomic_type) { - $atomic_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); + $should_set = true; + $atomic_type = $atomic_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); if ($atomic_type instanceof TTemplateParam) { $template_type = self::replaceTemplateParam( @@ -69,7 +69,7 @@ public static function replace( ); if ($template_type) { - $keys_to_unset[] = $key; + $should_set = false; foreach ($template_type->getAtomicTypes() as $template_type_part) { if ($template_type_part instanceof TMixed) { @@ -114,11 +114,11 @@ public static function replace( } if ($class_template_type) { - $keys_to_unset[] = $key; + $should_set = false; $new_types[] = $class_template_type; } } elseif ($atomic_type instanceof TTemplateIndexedAccess) { - $keys_to_unset[] = $key; + $should_set = false; $template_type = null; @@ -176,7 +176,7 @@ public static function replace( ); if ($new_type) { - $keys_to_unset[] = $key; + $should_set = false; $new_types[] = $new_type; } } elseif ($atomic_type instanceof TTemplatePropertiesOf) { @@ -187,7 +187,7 @@ public static function replace( ); if ($new_type) { - $keys_to_unset[] = $key; + $should_set = false; $new_types[] = $new_type; } } elseif ($atomic_type instanceof TConditional @@ -200,22 +200,24 @@ public static function replace( $inferred_lower_bounds ); - $keys_to_unset[] = $key; + $should_set = false; foreach ($class_template_type->getAtomicTypes() as $class_template_atomic_type) { $new_types[] = $class_template_atomic_type; } } - } - $union->bustCache(); + if ($should_set) { + $types []= $atomic_type; + } + } if ($is_mixed) { if (!$new_types) { throw new UnexpectedValueException('This array should be full'); } - return $union->replaceTypes( + return $union->getBuilder()->setTypes( TypeCombiner::combine( $new_types, $codebase @@ -223,13 +225,12 @@ public static function replace( )->freeze(); } - foreach ($keys_to_unset as $key) { - $union->removeType($key); + $atomic_types = array_merge($types, $new_types); + if (!$atomic_types) { + throw new UnexpectedValueException('This array should be full'); } - $atomic_types = array_values(array_merge($union->getAtomicTypes(), $new_types)); - - return $union->replaceTypes( + return $union->getBuilder()->setTypes( TypeCombiner::combine( $atomic_types, $codebase @@ -259,36 +260,36 @@ private static function replaceTemplateParam( if ($traversed_type) { $template_type = $traversed_type; - if (!$atomic_type->as->isMixed() && $template_type->isMixed()) { - $template_type = $atomic_type->as->getBuilder(); - } else { - $template_type = $template_type->getBuilder(); + if ($template_type->isMixed() && !$atomic_type->as->isMixed()) { + $template_type = $atomic_type->as; } if ($atomic_type->extra_types) { - foreach ($template_type->getAtomicTypes() as $template_type_key => $atomic_template_type) { + $types = []; + foreach ($template_type->getAtomicTypes() as $atomic_template_type) { if ($atomic_template_type instanceof TNamedObject || $atomic_template_type instanceof TTemplateParam || $atomic_template_type instanceof TIterable || $atomic_template_type instanceof TObjectWithProperties ) { - $atomic_template_type->extra_types = array_merge( + $types []= $atomic_template_type->setIntersectionTypes(array_merge( $atomic_type->extra_types, - $atomic_template_type->extra_types ?: [] - ); + $atomic_template_type->extra_types + )); } elseif ($atomic_template_type instanceof TObject) { $first_atomic_type = array_shift($atomic_type->extra_types); if ($atomic_type->extra_types) { - $first_atomic_type->extra_types = $atomic_type->extra_types; + $first_atomic_type = $first_atomic_type->setIntersectionTypes($atomic_type->extra_types); } - $template_type->removeType($template_type_key); - $template_type->addType($first_atomic_type); + $types []= $first_atomic_type; + } else { + $types []= $atomic_template_type; } } + $template_type = $template_type->getBuilder()->setTypes($types)->freeze(); } - $template_type = $template_type->freeze(); } elseif ($codebase) { foreach ($inferred_lower_bounds as $template_type_map) { foreach ($template_type_map as $template_class => $_) { @@ -382,7 +383,6 @@ private static function replaceTemplatePropertiesOf( } return new TPropertiesOf( - (string) $classlike_type, clone $classlike_type, $atomic_type->visibility_filter ); diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 32c3afaa714..8ed8450b820 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -54,6 +54,39 @@ */ class TemplateStandinTypeReplacer { + /** + * This method fills in the values in $template_result based on how the various atomic types + * of $union_type match up to the types inside $input_type. + */ + public static function fillTemplateResult( + Union $union_type, + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer, + ?Union $input_type, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + ?string $bound_equality_classlike = null, + int $depth = 1 + ): void { + self::replace( + $union_type, + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $bound_equality_classlike, + $depth + ); + } /** * This replaces template types in unions with standins (normally the template as type) * @@ -341,7 +374,6 @@ private static function handleAtomicStandin( } $atomic_type = new TPropertiesOf( - (string) $classlike_type, clone $classlike_type, $atomic_type->visibility_filter ); diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index c7770a32c26..2d7c74b4af5 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -81,9 +81,9 @@ class TypeCombination public $class_string_types = []; /** - * @var array|null + * @var array */ - public $extra_types; + public $extra_types = []; /** @var ?bool */ public $all_arrays_lists; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 6d25b1f40c2..b8e3f18ca0f 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -511,7 +511,7 @@ private static function scrapeTypeProperties( ) { if ($type->extra_types) { $combination->extra_types = array_merge( - $combination->extra_types ?: [], + $combination->extra_types, $type->extra_types ); } diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index babaf30aa77..5d831be2cef 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -5,11 +5,11 @@ use Psalm\Codebase; use Psalm\Exception\CircularReferenceException; use Psalm\Exception\UnresolvableConstantException; +use Psalm\Internal\Analyzer\Statements\Expression\Fetch\AtomicPropertyFetchAnalyzer; use Psalm\Internal\Type\SimpleAssertionReconciler; use Psalm\Internal\Type\SimpleNegatedAssertionReconciler; use Psalm\Internal\Type\TypeParser; use Psalm\Storage\Assertion\IsType; -use Psalm\Storage\PropertyStorage; use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; @@ -130,6 +130,7 @@ public static function expandUnion( * * @return non-empty-list * + * @psalm-suppress ConflictingReferenceConstraint Ultimately, the output type is always an Atomic * @psalm-suppress ComplexMethod */ public static function expandAtomic( @@ -644,9 +645,8 @@ private static function expandNamedObject( || $static_class_type instanceof TTemplateParam) ) { $return_type = clone $return_type; - $cloned_static = clone $static_class_type; - $extra_static = $cloned_static->extra_types ?: []; - $cloned_static->extra_types = null; + $extra_static = $static_class_type->extra_types; + $cloned_static = $static_class_type->setIntersectionTypes([]); if ($cloned_static->getKey(false) !== $return_type->getKey(false)) { $return_type->extra_types[$static_class_type->getKey()] = clone $cloned_static; @@ -892,62 +892,65 @@ private static function expandConditional( */ private static function expandPropertiesOf( Codebase $codebase, - TPropertiesOf $return_type, + TPropertiesOf &$return_type, ?string $self_class, $static_class_type ): array { - if ($return_type->fq_classlike_name === 'self' && $self_class) { - $return_type->fq_classlike_name = $self_class; + if ($self_class) { + $return_type = $return_type->replaceClassLike('self', $self_class); + $return_type = $return_type->replaceClassLike( + 'static', + is_string($static_class_type) ? $static_class_type : $self_class + ); } - if ($return_type->fq_classlike_name === 'static' && $self_class) { - $return_type->fq_classlike_name = is_string($static_class_type) ? $static_class_type : $self_class; + $class_storage = null; + if ($codebase->classExists($return_type->classlike_type->value)) { + $class_storage = $codebase->classlike_storage_provider->get($return_type->classlike_type->value); + } else { + foreach ($return_type->classlike_type->extra_types as $type) { + if ($type instanceof TNamedObject && $codebase->classExists($type->value)) { + $class_storage = $codebase->classlike_storage_provider->get($type->value); + break; + } + } } - if (!$codebase->classExists($return_type->fq_classlike_name)) { + if (!$class_storage) { return [$return_type]; } - // Get and merge all properties from parent classes - $class_storage = $codebase->classlike_storage_provider->get($return_type->fq_classlike_name); - $properties_types = $class_storage->properties; - foreach ($class_storage->parent_classes as $parent_class) { - if (!$codebase->classOrInterfaceExists($parent_class)) { + $properties = []; + foreach ([$class_storage->name, ...array_values($class_storage->parent_classes)] as $class) { + if (!$codebase->classExists($class)) { continue; } - $parent_class_storage = $codebase->classlike_storage_provider->get($parent_class); - $properties_types = array_merge( - $properties_types, - $parent_class_storage->properties - ); - } - - // Filter only non-static properties, and check visibility filter - $properties_types = array_filter( - $properties_types, - function (PropertyStorage $property) use ($return_type): bool { + $storage = $codebase->classlike_storage_provider->get($class); + foreach ($storage->properties as $key => $property) { + if (isset($properties[$key])) { + continue; + } if ($return_type->visibility_filter !== null && $property->visibility !== $return_type->visibility_filter ) { - return false; + continue; } - return !$property->is_static; - } - ); - - // Return property names as literal string - $properties = array_map( - function (PropertyStorage $property): ?Union { - return $property->type; - }, - $properties_types - ); - $properties = array_filter( - $properties, - function (?Union $property_type): bool { - return $property_type !== null; + if ($property->is_static || !$property->type) { + continue; + } + $type = $return_type->classlike_type instanceof TGenericObject + ? AtomicPropertyFetchAnalyzer::localizePropertyType( + $codebase, + $property->type, + $return_type->classlike_type, + $storage, + $storage + ) + : $property->type + ; + $properties[$key] = $type; } - ); + } if ($properties === []) { return [$return_type]; diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index a77c77093d5..711375ba150 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -703,6 +703,12 @@ private static function getTypeFromGenericTree( $generic_type_value . '<' . $param_name . '> must be a TTemplateParam.' ); } + if ($template_param->getIntersectionTypes()) { + throw new TypeParseTreeException( + $generic_type_value . '<' . $param_name . '> must be a TTemplateParam' + . ' with no intersection types.' + ); + } return new TTemplatePropertiesOf( $param_name, @@ -723,7 +729,6 @@ private static function getTypeFromGenericTree( } return new TPropertiesOf( - $param_name, $param_union_types[0], TPropertiesOf::filterForTokenName($generic_type_value) ); diff --git a/src/Psalm/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php index 4a5e0e7b2ce..f333dbeba93 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -144,6 +144,16 @@ public function getId(): string . ($this->is_optional ? '=' : ''); } + public function replaceType(Union $type): self + { + if ($this->type === $type) { + return $this; + } + $cloned = clone $this; + $cloned->type = $type; + return $cloned; + } + public function __clone() { if ($this->type) { diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 42db8658e2d..2e9670fd6af 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -787,11 +787,9 @@ private static function intersectAtomicTypes( $wider_type_intersection_types = $wider_type->getIntersectionTypes(); - if ($wider_type_intersection_types !== null) { - foreach ($wider_type_intersection_types as $wider_type_intersection_type) { - $intersection_atomic->extra_types[$wider_type_intersection_type->getKey()] - = clone $wider_type_intersection_type; - } + foreach ($wider_type_intersection_types as $wider_type_intersection_type) { + $intersection_atomic->extra_types[$wider_type_intersection_type->getKey()] + = clone $wider_type_intersection_type; } } diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index f76cd62e037..b95b789f0cb 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -20,7 +20,6 @@ use Psalm\Type\Atomic\TCallableList; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; -use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TClassStringMap; use Psalm\Type\Atomic\TClosedResource; @@ -113,6 +112,26 @@ abstract class Atomic implements TypeNode * @param array $type_aliases */ public static function create( + string $value, + ?int $analysis_php_version_id = null, + array $template_type_map = [], + array $type_aliases = [], + ?int $offset_start = null, + ?int $offset_end = null, + ?string $text = null + ): Atomic { + $result = self::createInner($value, $analysis_php_version_id, $template_type_map, $type_aliases); + $result->offset_start = $offset_start; + $result->offset_end = $offset_end; + $result->text = $text; + return $result; + } + /** + * @param int $analysis_php_version_id contains php version when the type comes from signature + * @param array> $template_type_map + * @param array $type_aliases + */ + private static function createInner( string $value, ?int $analysis_php_version_id = null, array $template_type_map = [], @@ -354,7 +373,7 @@ public function isNamedObjectType(): bool || ($this instanceof TTemplateParam && ($this->as->hasNamedObjectType() || array_filter( - $this->extra_types ?: [], + $this->extra_types, static fn($extra_type): bool => $extra_type->isNamedObjectType() ) ) @@ -517,77 +536,12 @@ public function getChildNodes(): array return []; } - public function replaceClassLike(string $old, string $new): void + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self { - if ($this instanceof TNamedObject) { - if (strtolower($this->value) === $old) { - $this->value = $new; - } - } - - if ($this instanceof TNamedObject - || $this instanceof TIterable - || $this instanceof TTemplateParam - ) { - if ($this->extra_types) { - foreach ($this->extra_types as $extra_type) { - $extra_type->replaceClassLike($old, $new); - } - } - } - - if ($this instanceof TClassConstant) { - if (strtolower($this->fq_classlike_name) === $old) { - $this->fq_classlike_name = $new; - } - } - - if ($this instanceof TClassString && $this->as !== 'object') { - if (strtolower($this->as) === $old) { - $this->as = $new; - } - } - - if ($this instanceof TTemplateParam) { - $this->as = $this->as->getBuilder()->replaceClassLike($old, $new)->freeze(); - } - - if ($this instanceof TLiteralClassString) { - if (strtolower($this->value) === $old) { - $this->value = $new; - } - } - - if ($this instanceof TArray - || $this instanceof TGenericObject - || $this instanceof TIterable - ) { - foreach ($this->type_params as &$type_param) { - $type_param = $type_param->getBuilder()->replaceClassLike($old, $new)->freeze(); - } - } - - if ($this instanceof TKeyedArray) { - foreach ($this->properties as &$property_type) { - $property_type = $property_type->getBuilder()->replaceClassLike($old, $new)->freeze(); - } - } - - if ($this instanceof TClosure - || $this instanceof TCallable - ) { - if ($this->params) { - foreach ($this->params as $param) { - if ($param->type) { - $param->type = $param->type->getBuilder()->replaceClassLike($old, $new)->freeze(); - } - } - } - - if ($this->return_type) { - $this->return_type = $this->return_type->getBuilder()->replaceClassLike($old, $new)->freeze(); - } - } + return $this; } final public function __toString(): string @@ -660,6 +614,9 @@ abstract public function toPhpString( abstract public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool; + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -672,14 +629,19 @@ public function replaceTemplateTypesWithStandins( bool $add_lower_bound = false, int $depth = 0 ): self { + // do nothing return $this; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { + ): self { // do nothing + return $this; } public function equals(Atomic $other_type, bool $ensure_source_equality): bool diff --git a/src/Psalm/Type/Atomic/CallableTrait.php b/src/Psalm/Type/Atomic/CallableTrait.php index 072c21957ec..328c4a01ce2 100644 --- a/src/Psalm/Type/Atomic/CallableTrait.php +++ b/src/Psalm/Type/Atomic/CallableTrait.php @@ -187,7 +187,10 @@ public function getId(bool $exact = true, bool $nested = false): string . $this->value . $param_string . $return_type_string; } - public function replaceTemplateTypesWithStandins( + /** + * @return array{list|null, Union|null}|null + */ + protected function replaceCallableTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, ?StatementsAnalyzer $statements_analyzer = null, @@ -198,11 +201,15 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $callable = clone $this; + ): ?array { + $replaced = false; + $params = $this->params; + if ($params) { + foreach ($params as $offset => &$param) { + if (!$param->type) { + continue; + } - if ($callable->params) { - foreach ($callable->params as $offset => $param) { $input_param_type = null; if (($input_type instanceof TClosure || $input_type instanceof TCallable) @@ -211,11 +218,7 @@ public function replaceTemplateTypesWithStandins( $input_param_type = $input_type->params[$offset]->type; } - if (!$param->type) { - continue; - } - - $param->type = TemplateStandinTypeReplacer::replace( + $new_param = $param->replaceType(TemplateStandinTypeReplacer::replace( $param->type, $template_result, $codebase, @@ -228,13 +231,16 @@ public function replaceTemplateTypesWithStandins( !$add_lower_bound, null, $depth - ); + )); + $replaced = $replaced || $new_param !== $param; + $param = $new_param; } } - if ($callable->return_type) { - $callable->return_type = TemplateStandinTypeReplacer::replace( - $callable->return_type, + $return_type = $this->return_type; + if ($return_type) { + $return_type = TemplateStandinTypeReplacer::replace( + $return_type, $template_result, $codebase, $statements_analyzer, @@ -247,42 +253,88 @@ public function replaceTemplateTypesWithStandins( $replace, $add_lower_bound ); + $replaced = $replaced || $this->return_type !== $return_type; } - return $callable; + if ($replaced) { + return [$params, $return_type]; + } + return null; } - public function replaceTemplateTypesWithArgTypes( + + /** + * @return array{list|null, Union|null}|null + */ + protected function replaceCallableTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - if ($this->params) { - foreach ($this->params as $param) { - if (!$param->type) { - continue; - } + ): ?array { + $replaced = false; - $param->type = TemplateInferredTypeReplacer::replace( - $param->type, - $template_result, - $codebase - ); + $params = $this->params; + if ($params) { + foreach ($params as &$param) { + if ($param->type) { + $new_param = $param->replaceType(TemplateInferredTypeReplacer::replace( + $param->type, + $template_result, + $codebase + )); + $replaced = $replaced || $new_param !== $param; + $param = $new_param; + } } } - if ($this->return_type) { - $this->return_type = TemplateInferredTypeReplacer::replace( - $this->return_type, + $return_type = $this->return_type; + if ($return_type) { + $return_type = TemplateInferredTypeReplacer::replace( + $return_type, $template_result, $codebase ); + $replaced = $replaced || $return_type !== $this->return_type; + } + if ($replaced) { + return [$params, $return_type]; + } + return null; + } + + /** + * @return array{list|null, Union|null}|null + */ + protected function replaceCallableClassLike(string $old, string $new): ?array + { + $replaced = false; + + $params = $this->params; + if ($params) { + foreach ($params as &$param) { + if ($param->type) { + $new_param = $param->replaceType($param->type->replaceClassLike($old, $new)); + $replaced = $replaced || $new_param !== $param; + $param = $new_param; + } + } + } + + $return_type = $this->return_type; + if ($return_type) { + $return_type = $return_type->replaceClassLike($old, $new); + $replaced = $replaced || $return_type !== $this->return_type; + } + if ($replaced) { + return [$params, $return_type]; } + return null; } /** * @return list */ - public function getChildNodes(): array + protected function getCallableChildNodes(): array { $child_nodes = []; diff --git a/src/Psalm/Type/Atomic/GenericTrait.php b/src/Psalm/Type/Atomic/GenericTrait.php index 78578107a27..d92861d1355 100644 --- a/src/Psalm/Type/Atomic/GenericTrait.php +++ b/src/Psalm/Type/Atomic/GenericTrait.php @@ -9,7 +9,6 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Type; use Psalm\Type\Atomic; -use Psalm\Type\TypeNode; use Psalm\Type\Union; use function array_map; @@ -19,8 +18,31 @@ use function strpos; use function substr; +/** + * @template TTypeParams as array + */ trait GenericTrait { + /** + * @var TTypeParams + */ + public array $type_params; + + /** + * @param TTypeParams $type_params + * + * @return static + */ + public function replaceTypeParams(array $type_params): self + { + if ($this->type_params === $type_params) { + return $this; + } + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + public function getId(bool $exact = true, bool $nested = false): string { $s = ''; @@ -147,14 +169,9 @@ public function __clone() } /** - * @return array + * @return TTypeParams|null */ - public function getChildNodes(): array - { - return $this->type_params; - } - - public function replaceTemplateTypesWithStandins( + protected function replaceTypeParamsTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, ?StatementsAnalyzer $statements_analyzer = null, @@ -165,7 +182,7 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { + ): ?array { if ($input_type instanceof TList) { $input_type = new TArray([Type::getInt(), $input_type->type_param]); } @@ -185,9 +202,9 @@ public function replaceTemplateTypesWithStandins( ); } - $atomic = clone $this; + $type_params = $this->type_params; - foreach ($atomic->type_params as $offset => $type_param) { + foreach ($type_params as $offset => $type_param) { $input_type_param = null; if (($input_type instanceof TIterable @@ -208,7 +225,7 @@ public function replaceTemplateTypesWithStandins( $input_type_param = $input_object_type_params[$offset]; } - $atomic->type_params[$offset] = TemplateStandinTypeReplacer::replace( + $type_params[$offset] = TemplateStandinTypeReplacer::replace( $type_param, $template_result, $codebase, @@ -227,14 +244,18 @@ public function replaceTemplateTypesWithStandins( ); } - return $atomic; + return $type_params === $this->type_params ? null : $type_params; } - public function replaceTemplateTypesWithArgTypes( + /** + * @return TTypeParams|null + */ + protected function replaceTypeParamsTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - foreach ($this->type_params as $offset => &$type_param) { + ): ?array { + $type_params = $this->type_params; + foreach ($type_params as $offset => &$type_param) { $type_param = TemplateInferredTypeReplacer::replace( $type_param, $template_result, @@ -242,16 +263,22 @@ public function replaceTemplateTypesWithArgTypes( ); if ($this instanceof TArray && $offset === 0 && $type_param->isMixed()) { - $this->type_params[0] = Type::getArrayKey(); + $type_param = Type::getArrayKey(); } } - if ($this instanceof TGenericObject) { - $this->remapped_params = true; - } + return $type_params === $this->type_params ? null : $type_params; + } - if ($this instanceof TGenericObject || $this instanceof TIterable) { - $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + /** + * @return TTypeParams|null + */ + protected function replaceTypeParamsClassLike(string $old, string $new): ?array + { + $type_params = $this->type_params; + foreach ($type_params as &$type_param) { + $type_param = $type_param->replaceClassLike($old, $new); } + return $type_params === $this->type_params ? null : $type_params; } } diff --git a/src/Psalm/Type/Atomic/HasIntersectionTrait.php b/src/Psalm/Type/Atomic/HasIntersectionTrait.php index 720379d9295..3f19aa9b9bd 100644 --- a/src/Psalm/Type/Atomic/HasIntersectionTrait.php +++ b/src/Psalm/Type/Atomic/HasIntersectionTrait.php @@ -3,19 +3,21 @@ namespace Psalm\Type\Atomic; use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Type\Atomic; use function array_map; +use function array_merge; use function implode; trait HasIntersectionTrait { /** - * @var array|null + * @var array */ - public $extra_types; + public array $extra_types = []; /** * @param array $aliased_classes @@ -49,26 +51,49 @@ private function getNamespacedIntersectionTypes( /** * @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties $type + * + * @return static */ - public function addIntersectionType(Atomic $type): void + public function addIntersectionType(Atomic $type): self { - $this->extra_types[$type->getKey()] = $type; + return $this->setIntersectionTypes(array_merge( + $this->extra_types, + [$type->getKey() => $type] + )); } /** - * @return array|null + * @param array $types + * + * @return static + */ + public function setIntersectionTypes(array $types): self + { + if ($types === $this->extra_types) { + return $this; + } + $cloned = clone $this; + $cloned->extra_types = $types; + return $cloned; + } + + /** + * @return array */ - public function getIntersectionTypes(): ?array + public function getIntersectionTypes(): array { return $this->extra_types; } - public function replaceIntersectionTemplateTypesWithArgTypes( + /** + * @return array|null + */ + protected function replaceIntersectionTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { + ): ?array { if (!$this->extra_types) { - return; + return null; } $new_types = []; @@ -90,11 +115,65 @@ public function replaceIntersectionTemplateTypesWithArgTypes( } } } else { - $extra_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); + $extra_type = $extra_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); $new_types[$extra_type->getKey()] = $extra_type; } } - $this->extra_types = $new_types; + return $new_types === $this->extra_types ? null : $new_types; + } + + /** + * @return array|null + */ + protected function replaceIntersectionTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): ?array { + if (!$this->extra_types) { + return null; + } + $new_types = []; + foreach ($this->extra_types as $type) { + $type = $type->replaceTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $new_types[$type->getKey()] = $type; + } + + return $new_types === $this->extra_types ? null : $new_types; + } + + /** + * @return array|null + */ + protected function replaceIntersectionClassLike(string $old, string $new): ?array + { + if (!$this->extra_types) { + return null; + } + $new_types = []; + foreach ($this->extra_types as $extra_type) { + $extra_type = $extra_type->replaceClassLike($old, $new); + $new_types[$extra_type->getKey()] = $extra_type; + } + return $new_types === $this->extra_types ? null : $new_types; } } diff --git a/src/Psalm/Type/Atomic/TAnonymousClassInstance.php b/src/Psalm/Type/Atomic/TAnonymousClassInstance.php index 1a29e4d27a4..1d77db1e5ef 100644 --- a/src/Psalm/Type/Atomic/TAnonymousClassInstance.php +++ b/src/Psalm/Type/Atomic/TAnonymousClassInstance.php @@ -14,10 +14,15 @@ final class TAnonymousClassInstance extends TNamedObject /** * @param string $value the name of the object + * @param array $extra_types */ - public function __construct(string $value, bool $is_static = false, ?string $extends = null) - { - parent::__construct($value, $is_static); + public function __construct( + string $value, + bool $is_static = false, + ?string $extends = null, + array $extra_types = [] + ) { + parent::__construct($value, $is_static, false, $extra_types); $this->extends = $extends; } diff --git a/src/Psalm/Type/Atomic/TArray.php b/src/Psalm/Type/Atomic/TArray.php index 88041e209a8..208ab12da29 100644 --- a/src/Psalm/Type/Atomic/TArray.php +++ b/src/Psalm/Type/Atomic/TArray.php @@ -2,6 +2,9 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -13,12 +16,10 @@ */ class TArray extends Atomic { - use GenericTrait; - /** - * @var array{Union, Union} + * @use GenericTrait */ - public $type_params; + use GenericTrait; /** * @var string @@ -96,4 +97,75 @@ public function isEmptyArray(): bool { return $this->type_params[1]->isNever(); } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type_params = $this->replaceTypeParamsClassLike($old, $new); + if ($type_params) { + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + return $this; + } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $type_params = $this->replaceTypeParamsTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if ($type_params) { + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + return $this; + } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self + { + $type_params = $this->replaceTypeParamsTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + if ($type_params) { + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + return $this; + } + + public function getChildNodes(): array + { + return $this->type_params; + } } diff --git a/src/Psalm/Type/Atomic/TCallable.php b/src/Psalm/Type/Atomic/TCallable.php index c0dc900af9e..8f4420bd1b6 100644 --- a/src/Psalm/Type/Atomic/TCallable.php +++ b/src/Psalm/Type/Atomic/TCallable.php @@ -2,6 +2,9 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type\Atomic; /** @@ -32,4 +35,79 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { return $this->params === null && $this->return_type === null; } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self + { + $replaced = $this->replaceCallableTemplateTypesWithArgTypes($template_result, $codebase); + if (!$replaced) { + return $this; + } + return new static( + $this->value, + $replaced[0], + $replaced[1], + $this->is_pure + ); + } + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $replaced = $this->replaceCallableTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$replaced) { + return $this; + } + return new static( + $this->value, + $replaced[0], + $replaced[1], + $this->is_pure + ); + } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $replaced = $this->replaceCallableClassLike($old, $new); + if (!$replaced) { + return $this; + } + return new static( + $this->value, + $replaced[0], + $replaced[1], + $this->is_pure + ); + } + + public function getChildNodes(): array + { + return $this->getCallableChildNodes(); + } } diff --git a/src/Psalm/Type/Atomic/TCallableString.php b/src/Psalm/Type/Atomic/TCallableString.php index 1f52a5da8a5..a3dc8036333 100644 --- a/src/Psalm/Type/Atomic/TCallableString.php +++ b/src/Psalm/Type/Atomic/TCallableString.php @@ -4,6 +4,7 @@ /** * Denotes the `callable-string` type, used to represent an unknown string that is also `callable`. + * */ final class TCallableString extends TNonFalsyString { diff --git a/src/Psalm/Type/Atomic/TClassConstant.php b/src/Psalm/Type/Atomic/TClassConstant.php index 79bd6497246..d85492f69a9 100644 --- a/src/Psalm/Type/Atomic/TClassConstant.php +++ b/src/Psalm/Type/Atomic/TClassConstant.php @@ -5,6 +5,8 @@ use Psalm\Type; use Psalm\Type\Atomic; +use function strtolower; + /** * Denotes a class constant whose value might not yet be known. */ @@ -22,6 +24,20 @@ public function __construct(string $fq_classlike_name, string $const_name) $this->const_name = $const_name; } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + if (strtolower($this->fq_classlike_name) === $old) { + return new TClassConstant( + $new, + $this->const_name + ); + } + return $this; + } + public function getKey(bool $include_extra = true): string { return 'class-constant(' . $this->fq_classlike_name . '::' . $this->const_name . ')'; diff --git a/src/Psalm/Type/Atomic/TClassString.php b/src/Psalm/Type/Atomic/TClassString.php index 1124c9e6a78..e040bf4a54e 100644 --- a/src/Psalm/Type/Atomic/TClassString.php +++ b/src/Psalm/Type/Atomic/TClassString.php @@ -42,12 +42,31 @@ class TClassString extends TString /** @var bool */ public $is_enum = false; - public function __construct(string $as = 'object', ?TNamedObject $as_type = null) - { + public function __construct( + string $as = 'object', + ?TNamedObject $as_type = null, + bool $is_loaded = false, + bool $is_interface = false, + bool $is_enum = false + ) { $this->as = $as; $this->as_type = $as_type; + $this->is_loaded = $is_loaded; + $this->is_interface = $is_interface; + $this->is_enum = $is_enum; + } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + if ($this->as !== 'object' && strtolower($this->as) === $old) { + $cloned = clone $this; + $cloned->as = $new; + return $cloned; + } + return $this; } - public function getKey(bool $include_extra = true): string { if ($this->is_interface) { @@ -133,6 +152,9 @@ public function getChildNodes(): array return $this->as_type ? [$this->as_type] : []; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -144,11 +166,9 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $class_string = clone $this; - - if (!$class_string->as_type) { - return $class_string; + ): self { + if (!$this->as_type) { + return $this; } if ($input_type instanceof TLiteralClassString) { @@ -160,7 +180,7 @@ public function replaceTemplateTypesWithStandins( } $as_type = TemplateStandinTypeReplacer::replace( - new Union([$class_string->as_type]), + new Union([$this->as_type]), $template_result, $codebase, $statements_analyzer, @@ -176,15 +196,19 @@ public function replaceTemplateTypesWithStandins( $as_type_types = array_values($as_type->getAtomicTypes()); - $class_string->as_type = count($as_type_types) === 1 + $as_type = count($as_type_types) === 1 && $as_type_types[0] instanceof TNamedObject ? $as_type_types[0] : null; - if (!$class_string->as_type) { - $class_string->as = 'object'; + if ($this->as_type === $as_type) { + return $this; } - - return $class_string; + $cloned = clone $this; + $cloned->as_type = $as_type; + if (!$cloned->as_type) { + $cloned->as = 'object'; + } + return $cloned; } } diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index 79dea6b4996..63e4faa96a1 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -39,9 +39,9 @@ final class TClassStringMap extends Atomic */ public function __construct(string $param_name, ?TNamedObject $as_type, Union $value_param) { - $this->value_param = $value_param; $this->param_name = $param_name; $this->as_type = $as_type; + $this->value_param = $value_param; } public function getId(bool $exact = true, bool $nested = false): string @@ -117,6 +117,9 @@ public function getKey(bool $include_extra = true): string return 'array'; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -128,10 +131,10 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $map = clone $this; + ): self { + $cloned = null; - foreach ([Type::getString(), $map->value_param] as $offset => $type_param) { + foreach ([Type::getString(), $this->value_param] as $offset => $type_param) { $input_type_param = null; if (($input_type instanceof TGenericObject @@ -170,23 +173,35 @@ public function replaceTemplateTypesWithStandins( $depth + 1 ); - if ($offset === 1) { - $map->value_param = $value_param; + if ($offset === 1 && ($cloned || $this->value_param !== $value_param)) { + $cloned ??= clone $this; + $cloned->value_param = $value_param; } } - return $map; + return $cloned ?? $this; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->value_param = TemplateInferredTypeReplacer::replace( + ): self { + $value_param = TemplateInferredTypeReplacer::replace( $this->value_param, $template_result, $codebase ); + if ($value_param === $this->value_param) { + return $this; + } + return new static( + $this->param_name, + $this->as_type, + $value_param + ); } public function getChildNodes(): array diff --git a/src/Psalm/Type/Atomic/TClosure.php b/src/Psalm/Type/Atomic/TClosure.php index fda5f17726e..f21afe32c09 100644 --- a/src/Psalm/Type/Atomic/TClosure.php +++ b/src/Psalm/Type/Atomic/TClosure.php @@ -2,6 +2,16 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\FunctionLikeParameter; +use Psalm\Type\Atomic; +use Psalm\Type\Union; + +use function array_merge; +use function strtolower; + /** * Represents a closure where we know the return type and params */ @@ -12,8 +22,129 @@ final class TClosure extends TNamedObject /** @var array */ public $byref_uses = []; + /** + * @param list $params + * @param array $byref_uses + * @param array $extra_types + */ + public function __construct( + string $value = 'callable', + ?array $params = null, + ?Union $return_type = null, + ?bool $is_pure = null, + array $byref_uses = [], + array $extra_types = [] + ) { + $this->value = $value; + $this->params = $params; + $this->return_type = $return_type; + $this->is_pure = $is_pure; + $this->byref_uses = $byref_uses; + $this->extra_types = $extra_types; + } + public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { return false; } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $replaced = $this->replaceCallableClassLike($old, $new); + $intersection = $this->replaceIntersectionClassLike($old, $new); + if (!$replaced && !$intersection) { + return $this; + } + return new static( + strtolower($this->value) === $old ? $new : $this->value, + $replaced[0] ?? $this->params, + $replaced[1] ?? $this->return_type, + $this->is_pure, + $this->byref_uses, + $intersection ?? $this->extra_types + ); + } + + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes( + TemplateResult $template_result, + ?Codebase $codebase + ): self { + $replaced = $this->replaceCallableTemplateTypesWithArgTypes($template_result, $codebase); + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + if (!$replaced && !$intersection) { + return $this; + } + return new static( + $this->value, + $replaced[0] ?? $this->params, + $replaced[1] ?? $this->return_type, + $this->is_pure, + $this->byref_uses, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $replaced = $this->replaceCallableTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$replaced && !$intersection) { + return $this; + } + return new static( + $this->value, + $replaced[0] ?? $this->params, + $replaced[1] ?? $this->return_type, + $this->is_pure, + $this->byref_uses, + $intersection ?? $this->extra_types + ); + } + + public function getChildNodes(): array + { + return array_merge(parent::getChildNodes(), $this->getCallableChildNodes()); + } } diff --git a/src/Psalm/Type/Atomic/TConditional.php b/src/Psalm/Type/Atomic/TConditional.php index 9b23b966450..e9caef03ddc 100644 --- a/src/Psalm/Type/Atomic/TConditional.php +++ b/src/Psalm/Type/Atomic/TConditional.php @@ -124,14 +124,28 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->conditional_type = TemplateInferredTypeReplacer::replace( + ): self { + $conditional = TemplateInferredTypeReplacer::replace( $this->conditional_type, $template_result, $codebase ); + if ($conditional === $this->conditional_type) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $this->as_type, + $conditional, + $this->if_type, + $this->else_type + ); } } diff --git a/src/Psalm/Type/Atomic/TDependentGetClass.php b/src/Psalm/Type/Atomic/TDependentGetClass.php index b1aacc2ceb0..c1b1b91fd41 100644 --- a/src/Psalm/Type/Atomic/TDependentGetClass.php +++ b/src/Psalm/Type/Atomic/TDependentGetClass.php @@ -2,7 +2,6 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; use Psalm\Type\Union; /** @@ -51,7 +50,7 @@ public function getVarId(): string return $this->typeof; } - public function getReplacement(): Atomic + public function getReplacement(): TClassString { return new TClassString(); } diff --git a/src/Psalm/Type/Atomic/TDependentGetDebugType.php b/src/Psalm/Type/Atomic/TDependentGetDebugType.php index aa51ceb6f84..e732cd3dbbe 100644 --- a/src/Psalm/Type/Atomic/TDependentGetDebugType.php +++ b/src/Psalm/Type/Atomic/TDependentGetDebugType.php @@ -2,8 +2,6 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; - /** * Represents a string whose value is that of a type found by get_debug_type($var) */ @@ -34,7 +32,7 @@ public function getVarId(): string return $this->typeof; } - public function getReplacement(): Atomic + public function getReplacement(): TString { return new TString(); } diff --git a/src/Psalm/Type/Atomic/TDependentListKey.php b/src/Psalm/Type/Atomic/TDependentListKey.php index 22f2e1c95bc..076217fe8c0 100644 --- a/src/Psalm/Type/Atomic/TDependentListKey.php +++ b/src/Psalm/Type/Atomic/TDependentListKey.php @@ -2,8 +2,6 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; - /** * Represents a list key created from foreach ($list as $key => $value) */ @@ -39,7 +37,7 @@ public function getAssertionString(): string return 'int'; } - public function getReplacement(): Atomic + public function getReplacement(): TInt { return new TInt(); } diff --git a/src/Psalm/Type/Atomic/TGenericObject.php b/src/Psalm/Type/Atomic/TGenericObject.php index 44f885e0757..ea0d00d343f 100644 --- a/src/Psalm/Type/Atomic/TGenericObject.php +++ b/src/Psalm/Type/Atomic/TGenericObject.php @@ -2,6 +2,9 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -9,6 +12,7 @@ use function count; use function implode; use function strrpos; +use function strtolower; use function substr; /** @@ -16,12 +20,10 @@ */ final class TGenericObject extends TNamedObject { - use GenericTrait; - /** - * @var non-empty-list + * @use GenericTrait> */ - public $type_params; + use GenericTrait; /** @var bool if the parameters have been remapped to another class */ public $remapped_params = false; @@ -29,15 +31,24 @@ final class TGenericObject extends TNamedObject /** * @param string $value the name of the object * @param non-empty-list $type_params + * @param array $extra_types */ - public function __construct(string $value, array $type_params) - { + public function __construct( + string $value, + array $type_params, + bool $remapped_params = false, + bool $is_static = false, + array $extra_types = [] + ) { if ($value[0] === '\\') { $value = substr($value, 1); } $this->value = $value; $this->type_params = $type_params; + $this->remapped_params = $remapped_params; + $this->is_static = $is_static; + $this->extra_types = $extra_types; } public function getKey(bool $include_extra = true): string @@ -105,6 +116,101 @@ public function getAssertionString(): string public function getChildNodes(): array { - return array_merge($this->type_params, $this->extra_types ?? []); + return array_merge(parent::getChildNodes(), $this->type_params); + } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type_params = $this->replaceTypeParamsClassLike($old, $new); + $intersection = $this->replaceIntersectionClassLike($old, $new); + if (!$type_params && !$intersection) { + return $this; + } + return new static( + strtolower($this->value) === $old ? $new : $this->value, + $type_params ?? $this->type_params, + $this->remapped_params, + $this->is_static, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $types = $this->replaceTypeParamsTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$types && !$intersection) { + return $this; + } + return new static( + $this->value, + $types ?? $this->type_params, + $this->remapped_params, + $this->is_static, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self + { + $type_params = $this->replaceTypeParamsTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + if (!$type_params && !$intersection) { + return $this; + } + return new static( + $this->value, + $type_params ?? $this->type_params, + true, + $this->is_static, + $intersection ?? $this->extra_types + ); } } diff --git a/src/Psalm/Type/Atomic/TIterable.php b/src/Psalm/Type/Atomic/TIterable.php index ed8462a5924..eca66a1b89f 100644 --- a/src/Psalm/Type/Atomic/TIterable.php +++ b/src/Psalm/Type/Atomic/TIterable.php @@ -2,11 +2,15 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Union; use function array_merge; +use function array_values; use function count; use function implode; use function substr; @@ -17,12 +21,10 @@ final class TIterable extends Atomic { use HasIntersectionTrait; - use GenericTrait; - /** - * @var array{Union, Union} + * @use GenericTrait */ - public $type_params; + use GenericTrait; /** * @var string @@ -35,16 +37,18 @@ final class TIterable extends Atomic public $has_docblock_params = false; /** - * @param list $type_params + * @param array{Union, Union}|array $type_params + * @param array $extra_types */ - public function __construct(array $type_params = []) + public function __construct(array $type_params = [], array $extra_types = []) { - if (count($type_params) === 2) { + if (isset($type_params[0], $type_params[1])) { $this->has_docblock_params = true; $this->type_params = $type_params; } else { $this->type_params = [Type::getMixed(), Type::getMixed()]; } + $this->extra_types = $extra_types; } public function getKey(bool $include_extra = true): string @@ -115,6 +119,94 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool public function getChildNodes(): array { - return array_merge($this->type_params, $this->extra_types ?? []); + return array_merge($this->type_params, array_values($this->extra_types)); + } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type_params = $this->replaceTypeParamsClassLike( + $old, + $new + ); + $intersection = $this->replaceIntersectionClassLike( + $old, + $new + ); + if (!$type_params && !$intersection) { + return $this; + } + return new static( + $type_params ?? $this->type_params, + $intersection ?? $this->extra_types + ); + } + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self + { + $type_params = $this->replaceTypeParamsTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + return new static( + $type_params ?? $this->type_params, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $types = $this->replaceTypeParamsTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$types && !$intersection) { + return $this; + } + return new static( + $types ?? $this->type_params, + $intersection ?? $this->extra_types + ); } } diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 45bb4eac01f..0235fba89b3 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -77,10 +77,47 @@ class TKeyedArray extends Atomic * @param non-empty-array $properties * @param array $class_strings */ - public function __construct(array $properties, ?array $class_strings = null) - { + public function __construct( + array $properties, + ?array $class_strings = null, + bool $sealed = false, + ?Union $previous_key_type = null, + ?Union $previous_value_type = null, + bool $is_list = false + ) { $this->properties = $properties; $this->class_strings = $class_strings; + $this->sealed = $sealed; + $this->previous_key_type = $previous_key_type; + $this->previous_value_type = $previous_value_type; + $this->is_list = $is_list; + } + + /** + * @param non-empty-array $properties + * + * @return static + */ + public function setProperties(array $properties): self + { + if ($properties === $this->properties) { + return $this; + } + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; + } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $properties = $this->properties; + foreach ($properties as &$property_type) { + $property_type = $property_type->replaceClassLike($old, $new); + } + return $this->setProperties($properties); } public function getId(bool $exact = true, bool $nested = false): string @@ -276,6 +313,9 @@ public function getKey(bool $include_extra = true): string return static::KEY; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -287,10 +327,10 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $object_like = clone $this; + ): self { + $properties = $this->properties; - foreach ($this->properties as $offset => $property) { + foreach ($properties as $offset => &$property) { $input_type_param = null; if ($input_type instanceof TKeyedArray @@ -299,7 +339,7 @@ public function replaceTemplateTypesWithStandins( $input_type_param = $input_type->properties[$offset]; } - $object_like->properties[$offset] = TemplateStandinTypeReplacer::replace( + $property = TemplateStandinTypeReplacer::replace( $property, $template_result, $codebase, @@ -315,20 +355,35 @@ public function replaceTemplateTypesWithStandins( ); } - return $object_like; + if ($properties === $this->properties) { + return $this; + } + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - foreach ($this->properties as &$property) { + ): self { + $properties = $this->properties; + foreach ($properties as &$property) { $property = TemplateInferredTypeReplacer::replace( $property, $template_result, $codebase ); } + if ($properties !== $this->properties) { + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; + } + return $this; } public function getChildNodes(): array diff --git a/src/Psalm/Type/Atomic/TList.php b/src/Psalm/Type/Atomic/TList.php index 4b33028560b..63c334cf6a1 100644 --- a/src/Psalm/Type/Atomic/TList.php +++ b/src/Psalm/Type/Atomic/TList.php @@ -18,6 +18,7 @@ * - its keys are integers * - they start at 0 * - they are consecutive and go upwards (no negative int) + * */ class TList extends Atomic { @@ -37,6 +38,19 @@ public function __construct(Union $type_param) $this->type_param = $type_param; } + /** + * @return static + */ + public function replaceTypeParam(Union $type_param): self + { + if ($type_param === $this->type_param) { + return $this; + } + $cloned = clone $this; + $cloned->type_param = $type_param; + return $cloned; + } + public function getId(bool $exact = true, bool $nested = false): string { return static::KEY . '<' . $this->type_param->getId($exact) . '>'; @@ -100,6 +114,9 @@ public function getKey(bool $include_extra = true): string return 'array'; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -111,10 +128,10 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $list = clone $this; + ): self { + $cloned = null; - foreach ([Type::getInt(), $list->type_param] as $offset => $type_param) { + foreach ([Type::getInt(), $this->type_param] as $offset => $type_param) { $input_type_param = null; if (($input_type instanceof TGenericObject @@ -153,23 +170,27 @@ public function replaceTemplateTypesWithStandins( $depth + 1 ); - if ($offset === 1) { - $list->type_param = $type_param; + if ($offset === 1 && ($cloned || $this->type_param !== $type_param)) { + $cloned ??= clone $this; + $cloned->type_param = $type_param; } } - return $list; + return $cloned ?? $this; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->type_param = TemplateInferredTypeReplacer::replace( + ): self { + return $this->replaceTypeParam(TemplateInferredTypeReplacer::replace( $this->type_param, $template_result, $codebase - ); + )); } public function equals(Atomic $other_type, bool $ensure_source_equality): bool diff --git a/src/Psalm/Type/Atomic/TLiteralClassString.php b/src/Psalm/Type/Atomic/TLiteralClassString.php index 6339a5757b3..735d45ba3ac 100644 --- a/src/Psalm/Type/Atomic/TLiteralClassString.php +++ b/src/Psalm/Type/Atomic/TLiteralClassString.php @@ -61,6 +61,17 @@ public function getAssertionString(): string return $this->getKey(); } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + if (strtolower($this->value) === $old) { + return new static($new, $this->definite_class); + } + return $this; + } + /** * @param array $aliased_classes */ diff --git a/src/Psalm/Type/Atomic/TLowercaseString.php b/src/Psalm/Type/Atomic/TLowercaseString.php index a9eecb9f362..92afd724299 100644 --- a/src/Psalm/Type/Atomic/TLowercaseString.php +++ b/src/Psalm/Type/Atomic/TLowercaseString.php @@ -2,6 +2,8 @@ namespace Psalm\Type\Atomic; +/** + */ final class TLowercaseString extends TString { public function getId(bool $exact = true, bool $nested = false): string diff --git a/src/Psalm/Type/Atomic/TMixed.php b/src/Psalm/Type/Atomic/TMixed.php index 0c029645a47..1fb883003b1 100644 --- a/src/Psalm/Type/Atomic/TMixed.php +++ b/src/Psalm/Type/Atomic/TMixed.php @@ -6,6 +6,7 @@ /** * Denotes the `mixed` type, used when you don’t know the type of an expression. + * */ class TMixed extends Atomic { diff --git a/src/Psalm/Type/Atomic/TNamedObject.php b/src/Psalm/Type/Atomic/TNamedObject.php index 88643acd73f..77707a699ab 100644 --- a/src/Psalm/Type/Atomic/TNamedObject.php +++ b/src/Psalm/Type/Atomic/TNamedObject.php @@ -3,13 +3,16 @@ namespace Psalm\Type\Atomic; use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Type; use Psalm\Type\Atomic; use function array_map; +use function array_values; use function implode; use function strrpos; +use function strtolower; use function substr; /** @@ -37,9 +40,14 @@ class TNamedObject extends Atomic /** * @param string $value the name of the object + * @param array $extra_types */ - public function __construct(string $value, bool $is_static = false, bool $definite_class = false) - { + public function __construct( + string $value, + bool $is_static = false, + bool $definite_class = false, + array $extra_types = [] + ) { if ($value[0] === '\\') { $value = substr($value, 1); } @@ -47,6 +55,7 @@ public function __construct(string $value, bool $is_static = false, bool $defini $this->value = $value; $this->is_static = $is_static; $this->definite_class = $definite_class; + $this->extra_types = $extra_types; } public function getKey(bool $include_extra = true): string @@ -134,15 +143,75 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return ($this->value !== 'static' && $this->is_static === false) || $analysis_php_version_id >= 8_00_00; } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $intersection = $this->replaceIntersectionClassLike($old, $new); + if (!$intersection && strtolower($this->value) !== $old) { + return $this; + } + $cloned = clone $this; + if (strtolower($cloned->value) === $old) { + $cloned->value = $new; + } + $cloned->extra_types = $intersection ?? $this->extra_types; + return $cloned; + } + + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + ): self { + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + if (!$intersection) { + return $this; + } + $cloned = clone $this; + $cloned->extra_types = $intersection; + return $cloned; } + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if ($intersection) { + $cloned = clone $this; + $cloned->extra_types = $intersection; + return $cloned; + } + return $this; + } public function getChildNodes(): array { - return $this->extra_types ?? []; + return array_values($this->extra_types); } } diff --git a/src/Psalm/Type/Atomic/TNonEmptyArray.php b/src/Psalm/Type/Atomic/TNonEmptyArray.php index d81b5dfe9c8..ed24494a74d 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyArray.php +++ b/src/Psalm/Type/Atomic/TNonEmptyArray.php @@ -2,6 +2,8 @@ namespace Psalm\Type\Atomic; +use Psalm\Type\Union; + /** * Denotes array known to be non-empty of the form `non-empty-array`. * It expects an array with two elements, both union types. @@ -22,4 +24,21 @@ class TNonEmptyArray extends TArray * @var string */ public $value = 'non-empty-array'; + + /** + * @param array{Union, Union} $type_params + * @param positive-int|null $count + * @param positive-int|null $min_count + */ + public function __construct( + array $type_params, + ?int $count = null, + ?int $min_count = null, + string $value = 'non-empty-array' + ) { + $this->type_params = $type_params; + $this->count = $count; + $this->min_count = $min_count; + $this->value = $value; + } } diff --git a/src/Psalm/Type/Atomic/TNonEmptyList.php b/src/Psalm/Type/Atomic/TNonEmptyList.php index 9e6892ba855..3a73f9790f2 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyList.php +++ b/src/Psalm/Type/Atomic/TNonEmptyList.php @@ -2,6 +2,8 @@ namespace Psalm\Type\Atomic; +use Psalm\Type\Union; + /** * Represents a non-empty list */ @@ -20,6 +22,19 @@ class TNonEmptyList extends TList /** @var non-empty-lowercase-string */ public const KEY = 'non-empty-list'; + /** + * Constructs a new instance of a list + * + * @param positive-int|null $count + * @param positive-int|null $min_count + */ + public function __construct(Union $type_param, ?int $count = null, ?int $min_count = null) + { + $this->type_param = $type_param; + $this->count = $count; + $this->min_count = $min_count; + } + public function getAssertionString(): string { return 'non-empty-list'; diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index 0338bd8fc69..032029bec6f 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -39,11 +39,13 @@ final class TObjectWithProperties extends TObject * * @param array $properties * @param array $methods + * @param array $extra_types */ - public function __construct(array $properties, array $methods = []) + public function __construct(array $properties, array $methods = [], array $extra_types = []) { $this->properties = $properties; $this->methods = $methods; + $this->extra_types = $extra_types; } public function getId(bool $exact = true, bool $nested = false): string @@ -170,6 +172,9 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return true; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -181,8 +186,8 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $object_like = clone $this; + ): self { + $properties = []; foreach ($this->properties as $offset => $property) { $input_type_param = null; @@ -193,7 +198,7 @@ public function replaceTemplateTypesWithStandins( $input_type_param = $input_type->properties[$offset]; } - $object_like->properties[$offset] = TemplateStandinTypeReplacer::replace( + $properties[$offset] = TemplateStandinTypeReplacer::replace( $property, $template_result, $codebase, @@ -209,13 +214,51 @@ public function replaceTemplateTypesWithStandins( ); } - return $object_like; + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if ($properties === $this->properties && !$intersection) { + return $this; + } + return new static($properties, $this->methods, $intersection ?? $this->extra_types); } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $properties = $this->properties; + foreach ($properties as &$property) { + $property = $property->replaceClassLike($old, $new); + } + $intersection = $this->replaceIntersectionClassLike($old, $new); + if (!$intersection && $properties === $this->properties) { + return $this; + } + return new static( + $properties, + $this->methods, + $intersection ?? $this->extra_types + ); + } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { + ): self { + $properties = $this->properties; foreach ($this->properties as &$property) { $property = TemplateInferredTypeReplacer::replace( $property, @@ -223,11 +266,23 @@ public function replaceTemplateTypesWithArgTypes( $codebase ); } + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + if ($properties === $this->properties && !$intersection) { + return $this; + } + return new static( + $properties, + $this->methods, + $intersection ?? $this->extra_types + ); } public function getChildNodes(): array { - return array_merge($this->properties, $this->extra_types !== null ? array_values($this->extra_types) : []); + return array_merge($this->properties, array_values($this->extra_types)); } public function getAssertionString(): string diff --git a/src/Psalm/Type/Atomic/TPropertiesOf.php b/src/Psalm/Type/Atomic/TPropertiesOf.php index b3e009c6c92..7cbfbdee376 100644 --- a/src/Psalm/Type/Atomic/TPropertiesOf.php +++ b/src/Psalm/Type/Atomic/TPropertiesOf.php @@ -9,8 +9,9 @@ * their apropriate types as values. * * @psalm-type TokenName = 'properties-of'|'public-properties-of'|'protected-properties-of'|'private-properties-of' + * */ -class TPropertiesOf extends Atomic +final class TPropertiesOf extends Atomic { // These should match the values of // `Psalm\Internal\Analyzer\ClassLikeAnalyzer::VISIBILITY_*`, as they are @@ -19,10 +20,6 @@ class TPropertiesOf extends Atomic public const VISIBILITY_PROTECTED = 2; public const VISIBILITY_PRIVATE = 3; - /** - * @var string - */ - public $fq_classlike_name; /** * @var TNamedObject */ @@ -45,6 +42,17 @@ public static function tokenNames(): array ]; } + /** + * @param self::VISIBILITY_*|null $visibility_filter + */ + public function __construct( + TNamedObject $classlike_type, + ?int $visibility_filter + ) { + $this->classlike_type = $classlike_type; + $this->visibility_filter = $visibility_filter; + } + /** * @param TokenName $tokenName * @return self::VISIBILITY_*|null @@ -81,16 +89,18 @@ public static function tokenNameForFilter(?int $visibility_filter): string } /** - * @param self::VISIBILITY_*|null $visibility_filter + * @return static */ - public function __construct( - string $fq_classlike_name, - TNamedObject $classlike_type, - ?int $visibility_filter - ) { - $this->fq_classlike_name = $fq_classlike_name; - $this->classlike_type = $classlike_type; - $this->visibility_filter = $visibility_filter; + public function replaceClassLike(string $old, string $new): self + { + $replaced = $this->classlike_type->replaceClassLike($old, $new); + if ($replaced === $this->classlike_type) { + return $this; + } + return new static( + $replaced, + $this->visibility_filter + ); } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php index ac3edfd4717..f408ba22b94 100644 --- a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php +++ b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php @@ -4,6 +4,8 @@ use Psalm\Type\Atomic; +/** + */ final class TTemplateIndexedAccess extends Atomic { /** diff --git a/src/Psalm/Type/Atomic/TTemplateKeyOf.php b/src/Psalm/Type/Atomic/TTemplateKeyOf.php index 754063833a7..26ef36b955b 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -81,14 +81,25 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->as = TemplateInferredTypeReplacer::replace( + ): self { + $as = TemplateInferredTypeReplacer::replace( $this->as, $template_result, $codebase ); + if ($as === $this->as) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $as + ); } } diff --git a/src/Psalm/Type/Atomic/TTemplateParam.php b/src/Psalm/Type/Atomic/TTemplateParam.php index b34d9e4d49f..eca94f16c16 100644 --- a/src/Psalm/Type/Atomic/TTemplateParam.php +++ b/src/Psalm/Type/Atomic/TTemplateParam.php @@ -8,6 +8,8 @@ use Psalm\Type\Union; use function array_map; +use function array_merge; +use function array_values; use function implode; /** @@ -32,11 +34,31 @@ final class TTemplateParam extends Atomic */ public $defining_class; - public function __construct(string $param_name, Union $extends, string $defining_class) + /** + * @param array $extra_types + */ + public function __construct(string $param_name, Union $extends, string $defining_class, array $extra_types = []) { $this->param_name = $param_name; $this->as = $extends; $this->defining_class = $defining_class; + $this->extra_types = $extra_types; + } + + /** + * @return static + */ + public function replaceAs(Union $as): self + { + if ($as === $this->as) { + return $this; + } + return new static( + $this->param_name, + $as, + $this->defining_class, + $this->extra_types + ); } public function getKey(bool $include_extra = true): string @@ -115,7 +137,7 @@ public function toNamespacedString( public function getChildNodes(): array { - return [$this->as]; + return array_merge([$this->as], array_values($this->extra_types)); } public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool @@ -123,10 +145,40 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $intersection = $this->replaceIntersectionClassLike($old, $new); + $replaced = $this->as->replaceClassLike($old, $new); + if (!$intersection && $replaced === $this->as) { + return $this; + } + return new static( + $this->param_name, + $replaced, + $this->defining_class, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + ): self { + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + if (!$intersection) { + return $this; + } + return new static( + $this->param_name, + $this->as, + $this->defining_class, + $intersection + ); } } diff --git a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php index 5dd0f0d52c1..8caf397ee0a 100644 --- a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php +++ b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php @@ -76,14 +76,30 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->as = TemplateInferredTypeReplacer::replace( - new Union([$this->as]), - $template_result, - $codebase - )->getSingleAtomic(); + ): self { + $param = new TTemplateParam( + $this->as->param_name, + TemplateInferredTypeReplacer::replace( + new Union([$this->as]), + $template_result, + $codebase, + ), + $this->as->defining_class + ); + if ($param->as === $this->as->as) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $param, + $this->visibility_filter + ); } } diff --git a/src/Psalm/Type/Atomic/TTemplateValueOf.php b/src/Psalm/Type/Atomic/TTemplateValueOf.php index 23fe4d0711d..adcb287d559 100644 --- a/src/Psalm/Type/Atomic/TTemplateValueOf.php +++ b/src/Psalm/Type/Atomic/TTemplateValueOf.php @@ -81,14 +81,25 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->as = TemplateInferredTypeReplacer::replace( + ): self { + $as = TemplateInferredTypeReplacer::replace( $this->as, $template_result, $codebase ); + if ($as === $this->as) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $as + ); } } diff --git a/src/Psalm/Type/Atomic/TTypeAlias.php b/src/Psalm/Type/Atomic/TTypeAlias.php index 8e70ff71951..58853597c99 100644 --- a/src/Psalm/Type/Atomic/TTypeAlias.php +++ b/src/Psalm/Type/Atomic/TTypeAlias.php @@ -7,6 +7,8 @@ use function array_map; use function implode; +/** + */ final class TTypeAlias extends Atomic { /** diff --git a/src/Psalm/Type/MutableUnion.php b/src/Psalm/Type/MutableUnion.php index 9703ea3b100..85df70b29da 100644 --- a/src/Psalm/Type/MutableUnion.php +++ b/src/Psalm/Type/MutableUnion.php @@ -212,11 +212,41 @@ final class MutableUnion implements TypeNode, Stringable public $different = false; /** - * @param non-empty-array $types + * @param non-empty-array $types */ - public function replaceTypes(array $types): self + public function setTypes(array $types): self { - $this->types = $types; + $this->literal_float_types = []; + $this->literal_int_types = []; + $this->literal_string_types = []; + $this->typed_class_strings = []; + + $from_docblock = false; + + $keyed_types = []; + + foreach ($types as $type) { + $key = $type->getKey(); + $keyed_types[$key] = $type; + + if ($type instanceof TLiteralInt) { + $this->literal_int_types[$key] = $type; + } elseif ($type instanceof TLiteralString) { + $this->literal_string_types[$key] = $type; + } elseif ($type instanceof TLiteralFloat) { + $this->literal_float_types[$key] = $type; + } elseif ($type instanceof TClassString + && ($type->as_type || $type instanceof TTemplateParamClass) + ) { + $this->typed_class_strings[$key] = $type; + } + + $from_docblock = $from_docblock || $type->from_docblock; + } + + $this->types = $keyed_types; + $this->from_docblock = $from_docblock; + return $this; } @@ -410,7 +440,7 @@ public function substitute($old_type, $new_type = null): self public function replaceClassLike(string $old, string $new): self { foreach ($this->types as $key => $atomic_type) { - $atomic_type->replaceClassLike($old, $new); + $atomic_type = $atomic_type->replaceClassLike($old, $new); $this->removeType($key); $this->addType($atomic_type); diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 146ce582a20..61efdd1cc9e 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -229,4 +229,13 @@ public function getBuilder(): MutableUnion } return $union; } + + public function replaceClassLike(string $old, string $new): self + { + $types = $this->types; + foreach ($types as &$atomic_type) { + $atomic_type = $atomic_type->replaceClassLike($old, $new); + } + return $types === $this->types ? $this : $this->getBuilder()->setTypes($types)->freeze(); + } } diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index e69385648bc..b7f0d053399 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -754,10 +754,7 @@ private function assertParameter(array $normalizedEntry, ReflectionParameter $pa } } - /** - * - * @psalm-suppress UndefinedMethod - */ + /** @psalm-suppress UndefinedMethod */ public function assertEntryReturnType(ReflectionFunction $function, string $entryReturnType): void { if (version_compare(PHP_VERSION, '8.1.0', '>=')) { diff --git a/tests/PropertiesOfTest.php b/tests/PropertiesOfTest.php index 9f5d3668552..77d6c248d59 100644 --- a/tests/PropertiesOfTest.php +++ b/tests/PropertiesOfTest.php @@ -16,6 +16,44 @@ class PropertiesOfTest extends TestCase public function providerValidCodeParse(): iterable { return [ + 'propertiesOfIntersection' => [ + 'code' => ' + */ + function test1($a) {} + /** + * @psalm-suppress InvalidReturnType + * @return properties-of + */ + function test2() {} + /** + * @psalm-suppress InvalidReturnType + * @return properties-of + */ + function test3() {} + + /** @var i $i */ + assert($i instanceof b); + $result1 = test1($i); + $result2 = test2(); + $result3 = test3(); + ', + 'assertions' => [ + '$result1===' => 'array{a: int}', + '$result2===' => 'array{a: int}', + '$result3===' => 'array{a: int}', + ] + ], 'publicPropertiesOf' => [ 'code' => ' [ + 'code' => 'test($container); + + if ($container->expr) { + if (random_int(0, 1)) { + self::test( + $container, + ); + } + return $container->expr; + } + return 0; + } + + private static function test( + a $_, + ): void { + } + }' + ], 'noCrashTemplateInsideGenerator' => [ 'code' => ' [ + 'code' => ' + */ + function asArray($obj) { + /** @var properties-of */ + $properties = []; + return $properties; + } + + /** @template T */ + class A { + /** @var bool */ + private $b = true; + /** @var string */ + protected $c = "c"; + + /** @param T $a */ + public function __construct(public $a) {} + } + + $obj = new A(42); + $objAsArray = asArray($obj); + ', + 'assertions' => [ + '$objAsArray===' => 'array{a: 42, b: bool, c: string}' + ] + ], 'privatePropertiesPicksPrivate' => [ 'code' => '