From 8c57fd312aeaf6775912aa7b8deb00f8d0a47f5e Mon Sep 17 00:00:00 2001 From: klimick Date: Tue, 7 Nov 2023 14:50:05 +0300 Subject: [PATCH] Reimplemented contextual inference --- psalm-baseline.xml | 6 - src/Psalm/Context.php | 29 ++ src/Psalm/ContextualTypeResolver.php | 60 ++++ .../Internal/Analyzer/ClosureAnalyzer.php | 151 +++++----- ...ClosureAnalyzerContextualTypeExtractor.php | 36 +++ .../Statements/Expression/ArrayAnalyzer.php | 13 + .../ArrayAnalyzerContextualTypeExtractor.php | 69 +++++ .../Expression/Call/ArgumentsAnalyzer.php | 56 ++-- .../ArgumentsTemplateResultCollector.php | 235 +++++++++++++++ .../CallLikeContextualTypeExtractor.php | 92 ++++++ .../CollectedArgumentTemplates.php | 35 +++ .../FunctionLikeStorageProvider.php | 103 +++++++ .../Call/ArgumentsTemplateResultCollector.php | 274 ------------------ .../Expression/Call/FunctionCallAnalyzer.php | 43 ++- .../Call/Method/AtomicMethodCallAnalyzer.php | 8 +- .../ExistingAtomicMethodCallAnalyzer.php | 54 +++- .../Call/Method/MissingMethodCallHandler.php | 94 ++++-- .../Expression/Call/NewAnalyzer.php | 32 +- .../ExistingAtomicStaticCallAnalyzer.php | 34 ++- .../Statements/Expression/CallAnalyzer.php | 6 - .../Statements/ExpressionAnalyzer.php | 8 + .../Analyzer/Statements/ReturnAnalyzer.php | 18 ++ .../ReturnAnalyzerContextualTypeExtractor.php | 71 +++++ .../Internal/Analyzer/StatementsAnalyzer.php | 7 + .../LanguageServer/LanguageClient.php | 24 +- .../Comparator/CallableTypeComparator.php | 105 +++++-- .../TemplateContextualBoundsCollector.php | 189 ++++++++++++ src/Psalm/Internal/Type/TypeExpander.php | 45 ++- src/Psalm/Storage/ClassLikeStorage.php | 22 ++ tests/Config/PluginTest.php | 5 - 30 files changed, 1425 insertions(+), 499 deletions(-) create mode 100644 src/Psalm/ContextualTypeResolver.php create mode 100644 src/Psalm/Internal/Analyzer/ClosureAnalyzerContextualTypeExtractor.php create mode 100644 src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzerContextualTypeExtractor.php create mode 100644 src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/ArgumentsTemplateResultCollector.php create mode 100644 src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/CallLikeContextualTypeExtractor.php create mode 100644 src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/CollectedArgumentTemplates.php create mode 100644 src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/FunctionLikeStorageProvider.php delete mode 100644 src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplateResultCollector.php create mode 100644 src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzerContextualTypeExtractor.php create mode 100644 src/Psalm/Internal/Type/TemplateContextualBoundsCollector.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index eb421df3fb0..59257022513 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -315,12 +315,6 @@ children[1]]]> - - - $l[4] - $r[4] - - getArgs()[0]]]> diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php index bf73229560a..241ec682078 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -422,6 +422,11 @@ final class Context */ public $parent_remove_vars = []; + /** + * @var ContextualTypeResolver|null + */ + public $contextual_type_resolver = null; + /** @internal */ public function __construct(?string $self = null) { @@ -947,4 +952,28 @@ public function insideUse(): bool || $this->inside_throw || $this->inside_isset; } + + /** + * Returns all possible template definers in context (class, function or method). + * + * @return array + */ + public function getPossibleTemplateDefiners(): array + { + $who_can_define_templates = []; + + if ($this->calling_function_id !== null) { + $who_can_define_templates['fn-' . $this->calling_function_id] = true; + } + + if ($this->calling_method_id !== null) { + $who_can_define_templates['fn-' . $this->calling_method_id] = true; + } + + if ($this->self !== null) { + $who_can_define_templates[$this->self] = true; + } + + return $who_can_define_templates; + } } diff --git a/src/Psalm/ContextualTypeResolver.php b/src/Psalm/ContextualTypeResolver.php new file mode 100644 index 00000000000..0cda0dc8156 --- /dev/null +++ b/src/Psalm/ContextualTypeResolver.php @@ -0,0 +1,60 @@ +contextual_type = $contextual_type; + $this->template_result = $template_result; + $this->codebase = $codebase; + } + + public function getCodebase(): Codebase + { + return $this->codebase; + } + + /** + * @return ($type is Union ? self : null) + */ + public function withContextualType(?Union $type): ?self + { + return $type !== null + ? new self($type, $this->template_result, $this->codebase) + : null; + } + + public function resolve(): Union + { + return TemplateInferredTypeReplacer::replace( + $this->contextual_type, + $this->template_result, + $this->codebase, + ); + } + + public function fillTemplateResult(Union $input_type): void + { + TemplateStandinTypeReplacer::fillTemplateResult( + $this->contextual_type, + $this->template_result, + $this->codebase, + null, + $input_type, + ); + } +} diff --git a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php index 0063dc2317e..43753ab2730 100644 --- a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php @@ -9,14 +9,14 @@ use Psalm\Internal\Codebase\VariableUseGraph; use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\PhpVisitor\ShortClosureVisitor; +use Psalm\Internal\Type\Comparator\TypeComparisonResult; use Psalm\Internal\Type\Comparator\UnionTypeComparator; +use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\DuplicateParam; use Psalm\Issue\PossiblyUndefinedVariable; use Psalm\Issue\UndefinedVariable; use Psalm\IssueBuffer; use Psalm\Type; -use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; @@ -33,6 +33,8 @@ */ class ClosureAnalyzer extends FunctionLikeAnalyzer { + public ?Union $possibly_return_type = null; + /** * @param PhpParser\Node\Expr\Closure|PhpParser\Node\Expr\ArrowFunction $function */ @@ -78,13 +80,6 @@ public static function analyzeExpression( ): bool { $closure_analyzer = new ClosureAnalyzer($stmt, $statements_analyzer); - if ($context->inside_return) { - self::potentiallyInferTypesOnClosureFromParentReturnType( - $statements_analyzer, - $closure_analyzer, - ); - } - if ($stmt instanceof PhpParser\Node\Expr\Closure && self::analyzeClosureUses($statements_analyzer, $stmt, $context) === false ) { @@ -216,6 +211,12 @@ public static function analyzeExpression( $use_context->calling_method_id = $context->calling_method_id; $use_context->phantom_classes = $context->phantom_classes; + $closure_analyzer->possibly_return_type = self::inferParamTypesFromContextualTypeAndGetPossibleReturnType( + $context, + $statements_analyzer, + $closure_analyzer, + ); + $closure_analyzer->analyze($use_context, $statements_analyzer->node_data, $context, false); if ($closure_analyzer->inferred_impure @@ -360,82 +361,94 @@ public static function analyzeClosureUses( * * @see \Psalm\Tests\ReturnTypeTest:756 */ - private static function potentiallyInferTypesOnClosureFromParentReturnType( + private static function inferParamTypesFromContextualTypeAndGetPossibleReturnType( + Context $context, StatementsAnalyzer $statements_analyzer, ClosureAnalyzer $closure_analyzer - ): void { - $parent_source = $statements_analyzer->getSource(); - - // if not returning from inside a function, return - if (!$parent_source instanceof ClosureAnalyzer && !$parent_source instanceof FunctionAnalyzer) { - return; + ): ?Union { + if (!$context->contextual_type_resolver) { + return null; } - $closure_id = $closure_analyzer->getClosureId(); - $closure_storage = $statements_analyzer - ->getCodebase() - ->getFunctionLikeStorage($statements_analyzer, $closure_id); - - $parent_fn_storage = $parent_source->getFunctionLikeStorage($statements_analyzer); + $contextual_callable_type = ClosureAnalyzerContextualTypeExtractor::extract( + $context->contextual_type_resolver, + ); - if ($parent_fn_storage->return_type === null) { - return; + if ($contextual_callable_type === null) { + return null; } - // can't infer returned closure if the parent doesn't have a callable return type - if (!$parent_fn_storage->return_type->hasCallableType()) { - return; + if ($contextual_callable_type->params === null) { + return null; } - // cannot infer if we have union/intersection types - if (!$parent_fn_storage->return_type->isSingle()) { - return; - } + $codebase = $statements_analyzer->getCodebase(); - /** @var TClosure|TCallable $parent_callable_return_type */ - $parent_callable_return_type = $parent_fn_storage->return_type->getSingleAtomic(); + $calling_closure_storage = $codebase->getFunctionLikeStorage( + $statements_analyzer, + $closure_analyzer->getClosureId(), + ); - if ($parent_callable_return_type->params === null && $parent_callable_return_type->return_type === null) { - return; - } + foreach ($calling_closure_storage->params as $param_index => $calling_param) { + $contextual_param_type = $contextual_callable_type->params[$param_index]->type ?? null; - foreach ($closure_storage->params as $key => $param) { - $parent_param = $parent_callable_return_type->params[$key] ?? null; - $param->type = self::inferInnerClosureTypeFromParent( - $statements_analyzer->getCodebase(), - $param->type, - $parent_param->type ?? null, - ); - } + // No contextual type info. Don't infer. + if ($contextual_param_type === null) { + continue; + } - $closure_storage->return_type = self::inferInnerClosureTypeFromParent( - $statements_analyzer->getCodebase(), - $closure_storage->return_type, - $parent_callable_return_type->return_type, - ); + // Explicit docblock type. Don't infer. + if ($calling_param->type !== $calling_param->signature_type) { + continue; + } - if (!$closure_storage->template_types && $parent_callable_return_type->templates) { - $closure_storage->template_types = $parent_callable_return_type->getTemplateMap(); - } - } + $contextual_param_type = self::expandContextualType($codebase, $context, $contextual_param_type); + $type_comparison_result = new TypeComparisonResult(); + + if ($calling_param->type === null + || UnionTypeComparator::isContainedBy( + $codebase, + $contextual_param_type, + $calling_param->type, + false, + false, + $type_comparison_result, + ) + ) { + if ($type_comparison_result->to_string_cast) { + continue; + } - /** - * - If non parent type, do nothing - * - If no return type, infer from parent - * - If parent return type is more specific, infer from parent - * - else, do nothing - */ - private static function inferInnerClosureTypeFromParent( - Codebase $codebase, - ?Union $return_type, - ?Union $parent_return_type - ): ?Union { - if (!$parent_return_type) { - return $return_type; + $calling_param->type = $contextual_param_type; + $calling_param->type_inferred = true; + } } - if (!$return_type || UnionTypeComparator::isContainedBy($codebase, $parent_return_type, $return_type)) { - return $parent_return_type; + + if (!$calling_closure_storage->template_types && $contextual_callable_type->templates) { + $calling_closure_storage->template_types = $contextual_callable_type->getTemplateMap(); } - return $return_type; + + return $contextual_callable_type->return_type; + } + + private static function expandContextualType(Codebase $codebase, Context $context, Union $contextual_type): Union + { + $expand_templates = true; + $do_not_expand_template_defined_at = $context->getPossibleTemplateDefiners(); + + return TypeExpander::expandUnion( + $codebase, + $contextual_type, + null, + null, + null, + true, + false, + false, + false, + $expand_templates, + false, + $do_not_expand_template_defined_at, + ); } } diff --git a/src/Psalm/Internal/Analyzer/ClosureAnalyzerContextualTypeExtractor.php b/src/Psalm/Internal/Analyzer/ClosureAnalyzerContextualTypeExtractor.php new file mode 100644 index 00000000000..dd36b994a35 --- /dev/null +++ b/src/Psalm/Internal/Analyzer/ClosureAnalyzerContextualTypeExtractor.php @@ -0,0 +1,36 @@ +resolve() + ->getAtomicTypes(); + + foreach ($atomics as $atomic) { + if ($atomic instanceof TClosure || $atomic instanceof TCallable) { + $candidates[] = $atomic; + } + } + + return count($candidates) === 1 ? $candidates[0] : null; + } +} diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index fa1fb7f1248..b8ce7e25e46 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -364,10 +364,23 @@ private static function analyzeArrayItem( $key_type = new Union([$key_atomic_type]); } + $was_contextual_type_resolver = $context->contextual_type_resolver; + + $context->contextual_type_resolver = $context->contextual_type_resolver !== null + ? ArrayAnalyzerContextualTypeExtractor::extract( + $item_key_type ?? Type::getInt(false, $array_creation_info->int_offset), + $context->contextual_type_resolver, + ) + : null; + if (ExpressionAnalyzer::analyze($statements_analyzer, $item->value, $context) === false) { + $context->contextual_type_resolver = $was_contextual_type_resolver; + return; } + $context->contextual_type_resolver = $was_contextual_type_resolver; + $array_creation_info->all_list = $array_creation_info->all_list && $item_is_list_item; if ($item_key_value !== null) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzerContextualTypeExtractor.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzerContextualTypeExtractor.php new file mode 100644 index 00000000000..72ce0c4caa2 --- /dev/null +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzerContextualTypeExtractor.php @@ -0,0 +1,69 @@ +getCodebase(); + $contextual_type = $contextual_type_resolver->resolve(); + + if (!$contextual_type->hasArray()) { + return null; + } + + $contextual_atomic = $contextual_type->getArray(); + + if ($contextual_atomic instanceof TArray) { + $contextual_key_type = $contextual_atomic->type_params[0]; + + if ($contextual_key_type->isSingle()) { + $contextual_key_atomic = $contextual_key_type->getSingleAtomic(); + + if ($contextual_key_atomic instanceof TTemplateParam) { + $contextual_key_type = $contextual_key_atomic->as; + } + } + + return $codebase->isTypeContainedByType($array_key_type, $contextual_key_type) + ? $contextual_type_resolver->withContextualType($contextual_atomic->type_params[1]) + : null; + } + + if ($contextual_atomic instanceof TKeyedArray) { + if ($array_key_type->isInt() && $contextual_atomic->isGenericList()) { + return $contextual_type_resolver->withContextualType($contextual_atomic->getGenericValueType()); + } + + if ($array_key_type->isSingleStringLiteral()) { + $literal = $array_key_type->getSingleStringLiteral()->value; + + return isset($contextual_atomic->properties[$literal]) + ? $contextual_type_resolver->withContextualType($contextual_atomic->properties[$literal]) + : null; + } + + if ($array_key_type->isSingleIntLiteral()) { + $literal = $array_key_type->getSingleIntLiteral()->value; + + return isset($contextual_atomic->properties[$literal]) + ? $contextual_type_resolver->withContextualType($contextual_atomic->properties[$literal]) + : null; + } + } + + return null; + } +} diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 7679a7bcb7a..e1b4d24b3a3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -79,9 +79,11 @@ public static function analyze( ?array $function_params, ?string $method_id, bool $allow_named_args, - Context $context, - ?TemplateResult $template_result = null + Context $context ): ?bool { + // @todo: Remove when plugins for array_map/array_filter will be implemented + $legacy_template_result = new TemplateResult([], []); + $last_param = $function_params ? $function_params[count($function_params) - 1] : null; @@ -172,6 +174,11 @@ public static function analyze( || $arg->value instanceof PhpParser\Node\Expr\BinaryOp ) ) { + $was_contextual_type_resolver = $context->contextual_type_resolver; + $context->contextual_type_resolver = $context->contextual_type_resolver !== null && isset($param->type) + ? $context->contextual_type_resolver->withContextualType($param->type) + : null; + if (self::handleByRefFunctionArg( $statements_analyzer, $method_id, @@ -179,9 +186,13 @@ public static function analyze( $arg, $context, ) === false) { + $context->contextual_type_resolver = $was_contextual_type_resolver; + return false; } + $context->contextual_type_resolver = $was_contextual_type_resolver; + continue; } @@ -199,29 +210,37 @@ public static function analyze( || $arg->value instanceof PhpParser\Node\Expr\ArrowFunction) && $param && !$arg->value->getDocComment() + && ($method_id === 'array_filter' || $method_id === 'array_map') ) { self::handleClosureArg( $statements_analyzer, $args, $method_id, $context, - $template_result ?? new TemplateResult([], []), + $legacy_template_result, $argument_offset, $arg, $param, ); } + $was_contextual_type_resolver = $context->contextual_type_resolver; + $context->contextual_type_resolver = $context->contextual_type_resolver !== null && isset($param->type) + ? $context->contextual_type_resolver->withContextualType($param->type) + : null; + $was_inside_call = $context->inside_call; $context->inside_call = true; if (ExpressionAnalyzer::analyze($statements_analyzer, $arg->value, $context) === false) { $context->inside_call = $was_inside_call; + $context->contextual_type_resolver = $was_contextual_type_resolver; return false; } $context->inside_call = $was_inside_call; + $context->contextual_type_resolver = $was_contextual_type_resolver; if (($argument_offset === 0 && $method_id === 'array_filter' && count($args) === 2) || ($argument_offset > 0 && $method_id === 'array_map' && count($args) >= 2) @@ -232,30 +251,7 @@ public static function analyze( $argument_offset, $arg, $context, - $template_result, - ); - } - - $inferred_arg_type = $statements_analyzer->node_data->getType($arg->value); - - if (null !== $inferred_arg_type - && null !== $template_result - && null !== $param - && null !== $param->type - && !$arg->unpack - ) { - $codebase = $statements_analyzer->getCodebase(); - $param_type = TemplateInferredTypeReplacer::replace($param->type, $template_result, $codebase); - - TemplateStandinTypeReplacer::fillTemplateResult( - $param_type, - $template_result, - $codebase, - $statements_analyzer, - $inferred_arg_type, - $argument_offset, - $context->self, - $context->calling_method_id ?: $context->calling_function_id, + $legacy_template_result, ); } @@ -283,7 +279,7 @@ private static function handleArrayMapFilterArrayArg( int $argument_offset, PhpParser\Node\Arg $arg, Context $context, - ?TemplateResult &$template_result + TemplateResult $template_result ): void { $codebase = $statements_analyzer->getCodebase(); @@ -319,10 +315,6 @@ private static function handleArrayMapFilterArrayArg( ); if ($replace_template_result->lower_bounds) { - if (!$template_result) { - $template_result = new TemplateResult([], []); - } - $template_result->lower_bounds += $replace_template_result->lower_bounds; } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/ArgumentsTemplateResultCollector.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/ArgumentsTemplateResultCollector.php new file mode 100644 index 00000000000..7c97ec0dd45 --- /dev/null +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/ArgumentsTemplateResultCollector.php @@ -0,0 +1,235 @@ +template_types ?? [] : [], + $function_storage !== null ? self::getClassTemplates($function_storage, $statements_analyzer) : [], + ), + $function_storage !== null && $lhs_type_part instanceof TNamedObject && !$lhs_type_part instanceof TClosure + ? array_merge( + self::getClassLowerBounds($stmt, $context, $statements_analyzer, $function_storage, $lhs_type_part), + self::getIfThisIsTypeLowerBounds($statements_analyzer, $function_storage, $lhs_type_part), + ) + : [], + ); + } + + /** + * @return array> + */ + private static function getClassTemplates( + FunctionLikeStorage $function_like_storage, + StatementsAnalyzer $statements_analyzer + ): array { + $codebase = $statements_analyzer->getCodebase(); + + if ($function_like_storage instanceof MethodStorage + && $function_like_storage->defining_fqcln + ) { + $classlike_storage = $codebase->classlikes->getStorageFor($function_like_storage->defining_fqcln); + + return $classlike_storage !== null + ? $classlike_storage->template_types ?? [] + : []; + } + + return []; + } + + /** + * @return array> + */ + private static function getClassLowerBounds( + CallLike $stmt, + Context $context, + StatementsAnalyzer $statements_analyzer, + FunctionLikeStorage $function_like_storage, + TNamedObject $lhs_type_part + ): array { + if ($function_like_storage->cased_name === null + || !$function_like_storage instanceof MethodStorage + || $function_like_storage->defining_fqcln === null + ) { + return []; + } + + $codebase = $statements_analyzer->getCodebase(); + + $fq_classlike_name = $codebase->classlikes->getUnAliasedName($lhs_type_part->value); + $method_name_lc = strtolower($function_like_storage->cased_name); + + $method_id = new MethodIdentifier( + $function_like_storage->defining_fqcln, + strtolower($function_like_storage->cased_name), + ); + + $self_call = !$statements_analyzer->isStatic() && $method_id->fq_class_name === $context->self; + + if ($self_call) { + $trait_lower_bounds = self::getTraitLowerBounds( + $codebase, + $statements_analyzer, + $lhs_type_part, + $fq_classlike_name, + $method_name_lc, + ); + + if ($trait_lower_bounds !== null) { + return $trait_lower_bounds; + } + } + + $lower_bounds = ClassTemplateParamCollector::collect( + $codebase, + $codebase->methods->getClassLikeStorageForMethod($method_id), + $codebase->classlike_storage_provider->get($fq_classlike_name), + $method_name_lc, + $lhs_type_part, + $self_call, + ) ?? []; + + $parent_call = $stmt instanceof StaticCall + && $stmt->class instanceof Name + && $stmt->class->getParts() === ['parent']; + + $template_extended_params = $parent_call && $context->self !== null + ? $codebase->classlike_storage_provider->get($context->self)->template_extended_params ?? [] + : []; + + foreach ($template_extended_params as $template_fq_class_name => $extended_types) { + foreach ($extended_types as $type_key => $extended_type) { + if (isset($lower_bounds[$type_key][$template_fq_class_name])) { + $lower_bounds[$type_key][$template_fq_class_name] = $extended_type; + continue; + } + + foreach ($extended_type->getAtomicTypes() as $t) { + $lower_bounds[$type_key][$template_fq_class_name] = + $t instanceof TTemplateParam && isset($lower_bounds[$t->param_name][$t->defining_class]) + ? $lower_bounds[$t->param_name][$t->defining_class] + : $extended_type; + } + } + } + + return $lower_bounds; + } + + /** + * @param lowercase-string $method_name_lc + * @return array> + */ + private static function getTraitLowerBounds( + Codebase $codebase, + StatementsAnalyzer $statements_analyzer, + TNamedObject $lhs_type_part, + string $fq_classlike_name, + string $method_name_lc + ): ?array { + $parent_source = $statements_analyzer->getSource(); + + if (!$parent_source instanceof FunctionLikeAnalyzer) { + return null; + } + + $grandparent_source = $parent_source->getSource(); + + if (!$grandparent_source instanceof TraitAnalyzer) { + return null; + } + + $fq_trait_name_lc = strtolower($grandparent_source->getFQCLN()); + $trait_storage = $codebase->classlike_storage_provider->get($fq_trait_name_lc); + + if (!isset($trait_storage->methods[$method_name_lc])) { + return null; + } + + $trait_method_id = new MethodIdentifier($trait_storage->name, $method_name_lc); + + return ClassTemplateParamCollector::collect( + $codebase, + $codebase->methods->getClassLikeStorageForMethod($trait_method_id), + $codebase->classlike_storage_provider->get($fq_classlike_name), + $method_name_lc, + $lhs_type_part, + true, + ) ?? []; + } + + /** + * @return array> + */ + private static function getIfThisIsTypeLowerBounds( + StatementsAnalyzer $statements_analyzer, + FunctionLikeStorage $function_like_storage, + TNamedObject $lhs_type_part + ): array { + $codebase = $statements_analyzer->getCodebase(); + + if (!$function_like_storage instanceof MethodStorage + || $function_like_storage->if_this_is_type === null + ) { + return []; + } + + $method_template_result = new TemplateResult($function_like_storage->template_types ?: [], []); + + TemplateStandinTypeReplacer::fillTemplateResult( + $function_like_storage->if_this_is_type, + $method_template_result, + $codebase, + $statements_analyzer, + new Union([$lhs_type_part]), + ); + + return array_map( + static fn(array $template_map): array => array_map( + static fn(array $lower_bounds): Union => TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( + $lower_bounds, + $codebase, + ), + $template_map, + ), + $method_template_result->lower_bounds, + ); + } +} diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/CallLikeContextualTypeExtractor.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/CallLikeContextualTypeExtractor.php new file mode 100644 index 00000000000..94854f549f7 --- /dev/null +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/CallLikeContextualTypeExtractor.php @@ -0,0 +1,92 @@ +contextual_type_resolver === null) { + $template_result_without_contextual_bounds = new TemplateResult( + $collected_templates->template_types, + $collected_templates->lower_bounds, + ); + + return new ContextualTypeResolver( + $empty_contextual_type, + $template_result_without_contextual_bounds, + $codebase, + ); + } + + $return_type = $function_storage->return_type + ?? self::getReturnTypeFromDeclaringConstructor($codebase, $function_storage); + + if ($return_type === null) { + $template_result_without_contextual_bounds = new TemplateResult( + $collected_templates->template_types, + $collected_templates->lower_bounds, + ); + + return new ContextualTypeResolver( + $empty_contextual_type, + $template_result_without_contextual_bounds, + $codebase, + ); + } + + $template_result_with_contextual_bounds = new TemplateResult( + $collected_templates->template_types, + array_merge($collected_templates->lower_bounds, TemplateContextualBoundsCollector::collect( + $codebase, + $context->contextual_type_resolver->resolve(), + $return_type, + )), + ); + + return new ContextualTypeResolver( + $return_type, + $template_result_with_contextual_bounds, + $codebase, + ); + } + + private static function getReturnTypeFromDeclaringConstructor( + Codebase $codebase, + FunctionLikeStorage $ctor_storage + ): ?Union { + if ($ctor_storage instanceof MethodStorage + && $ctor_storage->cased_name === '__construct' + && $ctor_storage->defining_fqcln !== null + ) { + $atomic = $codebase->classlike_storage_provider + ->get($ctor_storage->defining_fqcln) + ->getNamedObjectAtomic(); + + return $atomic !== null ? new Union([$atomic]) : null; + } + + return null; + } +} diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/CollectedArgumentTemplates.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/CollectedArgumentTemplates.php new file mode 100644 index 00000000000..afc2fc25ef1 --- /dev/null +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/CollectedArgumentTemplates.php @@ -0,0 +1,35 @@ +> + * @psalm-readonly + */ + public array $template_types; + + /** + * @var array> + * @psalm-readonly + */ + public array $lower_bounds; + + /** + * @param array> $template_types + * @param array> $lower_bounds + */ + public function __construct( + array $template_types = [], + array $lower_bounds = [] + ) { + $this->template_types = $template_types; + $this->lower_bounds = $lower_bounds; + } +} diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/FunctionLikeStorageProvider.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/FunctionLikeStorageProvider.php new file mode 100644 index 00000000000..fafc9b92eef --- /dev/null +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplate/FunctionLikeStorageProvider.php @@ -0,0 +1,103 @@ +getCodebase(), + $statements_analyzer, + ); + } + + if (($stmt instanceof MethodCall || $stmt instanceof StaticCall || $stmt instanceof New_) + && $function_like_id instanceof MethodIdentifier + && $function_like_id->fq_class_name !== 'object' + ) { + return self::getMethodStorage( + $function_like_id, + $statements_analyzer->getCodebase(), + ); + } + + return null; + } + + /** + * @param non-empty-string $function_like_id + */ + private static function getFunctionStorage( + FuncCall $stmt, + Context $context, + string $function_like_id, + Codebase $codebase, + StatementsAnalyzer $statements_analyzer + ): ?FunctionStorage { + $function_like_id_lc = strtolower($function_like_id); + + if ($codebase->functions->existence_provider->has($function_like_id_lc)) { + return null; + } + + if ($codebase->functions->dynamic_storage_provider->has($function_like_id_lc)) { + return $codebase->functions->dynamic_storage_provider->getFunctionStorage( + $stmt, + $statements_analyzer, + $function_like_id_lc, + $context, + new CodeLocation($statements_analyzer, $stmt), + ); + } + + if ($codebase->functions->functionExists($statements_analyzer, $function_like_id_lc)) { + return $codebase->functions->getStorage($statements_analyzer, $function_like_id_lc); + } + + return null; + } + + private static function getMethodStorage(MethodIdentifier $method_id, Codebase $codebase): ?MethodStorage + { + $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id); + + return $codebase->methods->hasStorage($declaring_method_id ?? $method_id) + ? $codebase->methods->getStorage($declaring_method_id ?? $method_id) + : null; + } +} diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplateResultCollector.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplateResultCollector.php deleted file mode 100644 index 1b53a4043fc..00000000000 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsTemplateResultCollector.php +++ /dev/null @@ -1,274 +0,0 @@ -template_types ?? [], - self::getClassTemplates($function_like_storage, $statements_analyzer), - ); - - $lower_bounds = $lhs_type_part instanceof TNamedObject - ? array_merge( - self::getClassLowerBounds($context, $statements_analyzer, $function_like_storage, $lhs_type_part), - self::getIfThisIsTypeLowerBounds($statements_analyzer, $function_like_storage, $lhs_type_part), - ) - : []; - - if ($call_parent && $context->self !== null) { - $codebase = $statements_analyzer->getCodebase(); - $self_cs = $codebase->classlike_storage_provider->get($context->self); - - foreach ($self_cs->template_extended_params ?? [] as $template_fq_class_name => $extended_types) { - foreach ($extended_types as $type_key => $extended_type) { - if (isset($lower_bounds[$type_key][$template_fq_class_name])) { - $lower_bounds[$type_key][$template_fq_class_name] = $extended_type; - continue; - } - - foreach ($extended_type->getAtomicTypes() as $t) { - if ($t instanceof TTemplateParam - && isset($lower_bounds[$t->param_name][$t->defining_class]) - ) { - $lower_bounds[$type_key][$template_fq_class_name] - = $lower_bounds[$t->param_name][$t->defining_class]; - } else { - $lower_bounds[$type_key][$template_fq_class_name] - = $extended_type; - break; - } - } - } - } - } - - return new TemplateResult($template_types, $lower_bounds); - } catch (Throwable $e) { - return new TemplateResult([], []); - } - } - - /** - * @param non-empty-string $function_like_id - */ - private static function getFunctionStorage( - string $function_like_id, - StatementsAnalyzer $statements_analyzer - ): ?FunctionLikeStorage { - if (false !== strpos($function_like_id, '::')) { - return null; - } - - $codebase = $statements_analyzer->getCodebase(); - $function_like_id_lc = strtolower($function_like_id); - - if ($codebase->functions->existence_provider->has($function_like_id_lc)) { - return null; - } - - return $codebase->functions->functionExists($statements_analyzer, $function_like_id_lc) - ? $codebase->functions->getStorage($statements_analyzer, $function_like_id_lc) - : null; - } - - /** - * @param non-empty-string $function_like_id - */ - private static function getMethodStorage( - string $function_like_id, - StatementsAnalyzer $statements_analyzer - ): ?FunctionLikeStorage { - if (false === strpos($function_like_id, '::')) { - return null; - } - - $function_like_id_parts = explode('::', ltrim($function_like_id, '\\')); - - if (!isset($function_like_id_parts[0]) || !isset($function_like_id_parts[1])) { - return null; - } - - $codebase = $statements_analyzer->getCodebase(); - [$class_name, $method_name] = $function_like_id_parts; - - $appearing_method_id = new MethodIdentifier( - $codebase->classlikes->getUnAliasedName($class_name), - strtolower($method_name), - ); - - $declaring_method_id = $codebase->methods->getDeclaringMethodId($appearing_method_id); - - try { - return $codebase->methods->getStorage($declaring_method_id ?? $appearing_method_id); - } catch (InvalidArgumentException $e) { - return null; - } - } - - /** - * @return array> - */ - private static function getClassTemplates( - FunctionLikeStorage $function_like_storage, - StatementsAnalyzer $statements_analyzer - ): array { - $codebase = $statements_analyzer->getCodebase(); - - if ($function_like_storage instanceof MethodStorage - && $function_like_storage->defining_fqcln - ) { - $classlike_storage = $codebase->classlikes->getStorageFor($function_like_storage->defining_fqcln); - - return $classlike_storage !== null - ? $classlike_storage->template_types ?? [] - : []; - } - - return []; - } - - /** - * @return array> - */ - private static function getClassLowerBounds( - Context $context, - StatementsAnalyzer $statements_analyzer, - FunctionLikeStorage $function_like_storage, - TNamedObject $lhs_type_part - ): array { - if ($function_like_storage->cased_name === null) { - return []; - } - - $codebase = $statements_analyzer->getCodebase(); - $parent_source = $statements_analyzer->getSource(); - - $fq_classlike_name = $codebase->classlikes->getUnAliasedName($lhs_type_part->value); - $method_name_lc = strtolower($function_like_storage->cased_name); - - $class_storage = $codebase->classlike_storage_provider->get($fq_classlike_name); - - $class_method_id = new MethodIdentifier($fq_classlike_name, $method_name_lc); - $self_call = !$statements_analyzer->isStatic() && $class_method_id->fq_class_name === $context->self; - - $class_lower_bounds = ClassTemplateParamCollector::collect( - $codebase, - $codebase->methods->getClassLikeStorageForMethod($class_method_id), - $class_storage, - $method_name_lc, - $lhs_type_part, - $self_call, - ) ?? []; - - if ($self_call && $parent_source instanceof FunctionLikeAnalyzer) { - $grandparent_source = $parent_source->getSource(); - - if (!$grandparent_source instanceof TraitAnalyzer) { - return $class_lower_bounds; - } - - $fq_trait_name_lc = strtolower($grandparent_source->getFQCLN()); - $trait_storage = $codebase->classlike_storage_provider->get($fq_trait_name_lc); - - if (!isset($trait_storage->methods[$method_name_lc])) { - return $class_lower_bounds; - } - - $trait_method_id = new MethodIdentifier($trait_storage->name, $method_name_lc); - - return ClassTemplateParamCollector::collect( - $codebase, - $codebase->methods->getClassLikeStorageForMethod($trait_method_id), - $class_storage, - $method_name_lc, - $lhs_type_part, - true, - ) ?? []; - } - - return $class_lower_bounds; - } - - /** - * @return array> - */ - private static function getIfThisIsTypeLowerBounds( - StatementsAnalyzer $statements_analyzer, - FunctionLikeStorage $function_like_storage, - TNamedObject $lhs_type_part - ): array { - $codebase = $statements_analyzer->getCodebase(); - - if ($function_like_storage instanceof MethodStorage && $function_like_storage->if_this_is_type !== null) { - $method_template_result = new TemplateResult($function_like_storage->template_types ?: [], []); - - TemplateStandinTypeReplacer::fillTemplateResult( - $function_like_storage->if_this_is_type, - $method_template_result, - $codebase, - $statements_analyzer, - new Union([$lhs_type_part]), - ); - - return array_map( - static fn(array $template_map): array => array_map( - static fn(array $lower_bounds): Union => TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( - $lower_bounds, - $codebase, - ), - $template_map, - ), - $method_template_result->lower_bounds, - ); - } - - return []; - } -} diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 32b4b7e6369..2fdfa523c1f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -10,6 +10,9 @@ use Psalm\Internal\Algebra\FormulaGenerator; use Psalm\Internal\Analyzer\AlgebraAnalyzer; use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\ArgumentsTemplateResultCollector; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\CallLikeContextualTypeExtractor; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\FunctionLikeStorageProvider; use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; @@ -19,6 +22,7 @@ use Psalm\Internal\DataFlow\TaintSink; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\Comparator\CallableTypeComparator; +use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TypeCombiner; use Psalm\Issue\DeprecatedFunction; use Psalm\Issue\ImpureFunctionCall; @@ -165,13 +169,36 @@ public static function analyze( $set_inside_conditional = true; } - if (!$is_first_class_callable) { - $template_result = ArgumentsTemplateResultCollector::collect( + $function_type = $statements_analyzer->node_data->getType($stmt->name); + + $function_storage = $function_call_info->function_id !== null + ? FunctionLikeStorageProvider::provide( + $stmt, $context, $statements_analyzer, $function_call_info->function_id, - ); + ) + : null; + + $collected_argument_templates = ArgumentsTemplateResultCollector::collect( + $stmt, + $context, + $statements_analyzer, + $function_storage, + $function_type !== null && $function_type->isSingle() + ? $function_type->getSingleAtomic() + : null, + ); + $was_contextual_resolver = $context->contextual_type_resolver; + $context->contextual_type_resolver = CallLikeContextualTypeExtractor::extract( + $context, + $codebase, + $function_storage, + $collected_argument_templates, + ); + + if (!$is_first_class_callable) { ArgumentsAnalyzer::analyze( $statements_analyzer, $stmt->getArgs(), @@ -179,7 +206,6 @@ public static function analyze( $function_call_info->function_id, $function_call_info->allow_named_args, $context, - $template_result, ); } @@ -205,10 +231,9 @@ public static function analyze( } } - $template_result = ArgumentsTemplateResultCollector::collect( - $context, - $statements_analyzer, - $function_call_info->function_id, + $template_result = new TemplateResult( + $collected_argument_templates->template_types, + $collected_argument_templates->lower_bounds, ); // do this here to allow closure param checks @@ -233,6 +258,8 @@ public static function analyze( ); } + $context->contextual_type_resolver = $was_contextual_resolver; + if ($function_name instanceof PhpParser\Node\Name && $function_call_info->function_id) { $stmt_type = FunctionCallReturnTypeFetcher::fetch( $statements_analyzer, 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 12c5197f3e8..0ad67931f0c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -908,8 +908,7 @@ private static function handleCallableObject( PhpParser\Node\Expr\MethodCall $stmt, Context $context, ?TCallable $lhs_type_part_callable, - AtomicMethodCallAnalysisResult $result, - ?TemplateResult $inferred_template_result = null + AtomicMethodCallAnalysisResult $result ): void { $method_id = 'object::__invoke'; $result->existent_method_ids[] = $method_id; @@ -928,8 +927,6 @@ private static function handleCallableObject( $result->too_many_arguments_method_ids[] = new MethodIdentifier('callable-object', '__invoke'); } - $template_result = $inferred_template_result ?? new TemplateResult([], []); - ArgumentsAnalyzer::analyze( $statements_analyzer, $stmt->getArgs(), @@ -937,7 +934,6 @@ private static function handleCallableObject( $method_id, false, $context, - $template_result, ); ArgumentsAnalyzer::checkArgumentsMatch( @@ -947,7 +943,7 @@ private static function handleCallableObject( $lhs_type_part_callable->params ?? [], null, null, - $template_result, + new TemplateResult([], []), new CodeLocation($statements_analyzer->getSource(), $stmt), $context, ); 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 9b1b3ea617a..929950ac1f5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -9,7 +9,9 @@ use Psalm\FileManipulation; use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentMapPopulator; -use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplateResultCollector; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\ArgumentsTemplateResultCollector; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\CallLikeContextualTypeExtractor; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\FunctionLikeStorageProvider; use Psalm\Internal\Analyzer\Statements\Expression\Call\FunctionCallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier; @@ -21,6 +23,7 @@ use Psalm\Internal\Type\Comparator\TypeComparisonResult; use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Internal\Type\TemplateInferredTypeReplacer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\IfThisIsMismatch; use Psalm\Issue\InvalidPropertyAssignmentValue; @@ -161,13 +164,26 @@ public static function analyze( $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id); - $template_result = ArgumentsTemplateResultCollector::collect( + $calling_method_storage = FunctionLikeStorageProvider::provide( + $stmt, + $context, + $statements_analyzer, + $method_id, + ); + + $collected_argument_templates = ArgumentsTemplateResultCollector::collect( + $stmt, $context, $statements_analyzer, - (string)$method_id, + $calling_method_storage, $lhs_type_part, ); + $template_result = new TemplateResult( + $collected_argument_templates->template_types, + $collected_argument_templates->lower_bounds, + ); + if ($codebase->store_node_types && !$stmt->isFirstClassCallable() && !$context->collect_initializations @@ -183,15 +199,29 @@ public static function analyze( $is_first_class_callable = $stmt->isFirstClassCallable(); - if (!$is_first_class_callable && self::checkMethodArgs( - $method_id, - $args, - $template_result, - $context, - new CodeLocation($source, $stmt_name), - $statements_analyzer, - ) === false) { - return Type::getMixed(); + if (!$is_first_class_callable) { + $was_contextual_type_resolver = $context->contextual_type_resolver; + $context->contextual_type_resolver = CallLikeContextualTypeExtractor::extract( + $context, + $codebase, + $calling_method_storage, + $collected_argument_templates, + ); + + if (self::checkMethodArgs( + $method_id, + $args, + $template_result, + $context, + new CodeLocation($source, $stmt_name), + $statements_analyzer, + ) === false) { + $context->contextual_type_resolver = $was_contextual_type_resolver; + + return Type::getMixed(); + } + + $context->contextual_type_resolver = $was_contextual_type_resolver; } $return_type_candidate = MethodCallReturnTypeFetcher::fetch( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php index 8e9346d803d..05baa7080f8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php @@ -8,6 +8,8 @@ use Psalm\Config; use Psalm\Context; use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsAnalyzer; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\CallLikeContextualTypeExtractor; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\CollectedArgumentTemplates; use Psalm\Internal\Analyzer\Statements\Expression\Call\ClassTemplateParamCollector; use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; @@ -114,13 +116,24 @@ public static function handleMagicMethod( [$pseudo_method_storage, $defining_class_storage] = $found_method_and_class_storage; - $found_generic_params = ClassTemplateParamCollector::collect( + $collected_argument_templates = new CollectedArgumentTemplates( + $defining_class_storage->template_types ?? [], + ClassTemplateParamCollector::collect( + $codebase, + $defining_class_storage, + $class_storage, + $method_name_lc, + $lhs_type_part, + !$statements_analyzer->isStatic() && $method_id->fq_class_name === $context->self, + ) ?? [], + ); + + $was_contextual_type_resolver = $context->contextual_type_resolver; + $context->contextual_type_resolver = CallLikeContextualTypeExtractor::extract( + $context, $codebase, - $defining_class_storage, - $class_storage, - $method_name_lc, - $lhs_type_part, - !$statements_analyzer->isStatic() && $method_id->fq_class_name === $context->self, + $pseudo_method_storage, + $collected_argument_templates, ); ArgumentsAnalyzer::analyze( @@ -130,7 +143,13 @@ public static function handleMagicMethod( (string) $method_id, true, $context, - $found_generic_params ? new TemplateResult([], $found_generic_params) : null, + ); + + $context->contextual_type_resolver = $was_contextual_type_resolver; + + $template_result = new TemplateResult( + $collected_argument_templates->template_types, + $collected_argument_templates->lower_bounds, ); ArgumentsAnalyzer::checkArgumentsMatch( @@ -140,7 +159,7 @@ public static function handleMagicMethod( $pseudo_method_storage->params, $pseudo_method_storage, null, - new TemplateResult([], $found_generic_params ?: []), + $template_result, new CodeLocation($statements_analyzer, $stmt), $context, ); @@ -148,13 +167,11 @@ public static function handleMagicMethod( if ($pseudo_method_storage->return_type) { $return_type_candidate = $pseudo_method_storage->return_type; - if ($found_generic_params) { - $return_type_candidate = TemplateInferredTypeReplacer::replace( - $return_type_candidate, - new TemplateResult([], $found_generic_params), - $codebase, - ); - } + $return_type_candidate = TemplateInferredTypeReplacer::replace( + $return_type_candidate, + $template_result, + $codebase, + ); $return_type_candidate = TypeExpander::expandUnion( $codebase, @@ -276,13 +293,24 @@ public static function handleMissingOrMagicMethod( return; } - $found_generic_params = ClassTemplateParamCollector::collect( + $collected_argument_templates = new CollectedArgumentTemplates( + $defining_class_storage->template_types ?? [], + ClassTemplateParamCollector::collect( + $codebase, + $defining_class_storage, + $class_storage, + $method_name_lc, + $lhs_type_part, + !$statements_analyzer->isStatic() && $method_id->fq_class_name === $context->self, + ) ?? [], + ); + + $was_contextual_type_resolver = $context->contextual_type_resolver; + $context->contextual_type_resolver = CallLikeContextualTypeExtractor::extract( + $context, $codebase, - $defining_class_storage, - $class_storage, - $method_name_lc, - $lhs_type_part, - !$statements_analyzer->isStatic() && $method_id->fq_class_name === $context->self, + $pseudo_method_storage, + $collected_argument_templates, ); if (ArgumentsAnalyzer::analyze( @@ -292,11 +320,19 @@ public static function handleMissingOrMagicMethod( (string) $method_id, true, $context, - $found_generic_params ? new TemplateResult([], $found_generic_params) : null, ) === false) { + $context->contextual_type_resolver = $was_contextual_type_resolver; + return; } + $context->contextual_type_resolver = $was_contextual_type_resolver; + + $template_result = new TemplateResult( + $collected_argument_templates->template_types, + $collected_argument_templates->lower_bounds, + ); + if (ArgumentsAnalyzer::checkArgumentsMatch( $statements_analyzer, $stmt->getArgs(), @@ -304,7 +340,7 @@ public static function handleMissingOrMagicMethod( $pseudo_method_storage->params, $pseudo_method_storage, null, - new TemplateResult([], $found_generic_params ?: []), + $template_result, new CodeLocation($statements_analyzer, $stmt->name), $context, ) === false) { @@ -314,13 +350,11 @@ public static function handleMissingOrMagicMethod( if ($pseudo_method_storage->return_type) { $return_type_candidate = $pseudo_method_storage->return_type; - if ($found_generic_params) { - $return_type_candidate = TemplateInferredTypeReplacer::replace( - $return_type_candidate, - new TemplateResult([], $found_generic_params), - $codebase, - ); - } + $return_type_candidate = TemplateInferredTypeReplacer::replace( + $return_type_candidate, + $template_result, + $codebase, + ); if ($all_intersection_return_type) { $return_type_candidate = Type::intersectUnionTypes( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index 1b52d64d9ce..5aead3bf218 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -11,6 +11,9 @@ use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; use Psalm\Internal\Analyzer\NamespaceAnalyzer; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\ArgumentsTemplateResultCollector; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\CallLikeContextualTypeExtractor; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\FunctionLikeStorageProvider; use Psalm\Internal\Analyzer\Statements\Expression\Call\Method\MethodVisibilityAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; @@ -390,10 +393,31 @@ private static function analyzeNamedConstructor( ); } - $template_result = ArgumentsTemplateResultCollector::collect( + $constructor_storage = FunctionLikeStorageProvider::provide( + $stmt, + $context, + $statements_analyzer, + $method_id, + ); + + $collected_argument_templates = ArgumentsTemplateResultCollector::collect( + $stmt, $context, $statements_analyzer, - (string)$method_id, + $constructor_storage, + ); + + $was_contextual_type_resolver = $context->contextual_type_resolver; + $context->contextual_type_resolver = CallLikeContextualTypeExtractor::extract( + $context, + $codebase, + $constructor_storage, + $collected_argument_templates, + ); + + $template_result = new TemplateResult( + $collected_argument_templates->template_types, + $collected_argument_templates->lower_bounds, ); if (self::checkMethodArgs( @@ -404,9 +428,13 @@ private static function analyzeNamedConstructor( new CodeLocation($statements_analyzer->getSource(), $stmt), $statements_analyzer, ) === false) { + $context->contextual_type_resolver = $was_contextual_type_resolver; + return; } + $context->contextual_type_resolver = $was_contextual_type_resolver; + if (MethodVisibilityAnalyzer::analyze( $method_id, $context, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index a33839f85a4..368df7c1bc1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -10,7 +10,9 @@ use Psalm\Context; use Psalm\FileManipulation; use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; -use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplateResultCollector; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\ArgumentsTemplateResultCollector; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\CallLikeContextualTypeExtractor; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsTemplate\FunctionLikeStorageProvider; use Psalm\Internal\Analyzer\Statements\Expression\Call\Method\MethodCallProhibitionAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Call\StaticCallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; @@ -144,12 +146,32 @@ public static function analyze( } } - $template_result = ArgumentsTemplateResultCollector::collect( + $calling_method_storage = FunctionLikeStorageProvider::provide( + $stmt, + $context, + $statements_analyzer, + $method_id, + ); + + $collected_argument_templates = ArgumentsTemplateResultCollector::collect( + $stmt, $context, $statements_analyzer, - (string)$method_id, + $calling_method_storage, $lhs_type_part, - $stmt->class instanceof PhpParser\Node\Name && $stmt->class->getParts() === ['parent'], + ); + + $was_contextual_type_resolver = $context->contextual_type_resolver; + $context->contextual_type_resolver = CallLikeContextualTypeExtractor::extract( + $context, + $codebase, + $calling_method_storage, + $collected_argument_templates, + ); + + $template_result = new TemplateResult( + $collected_argument_templates->template_types, + $collected_argument_templates->lower_bounds, ); if (CallAnalyzer::checkMethodArgs( @@ -160,9 +182,13 @@ public static function analyze( new CodeLocation($statements_analyzer->getSource(), $stmt), $statements_analyzer, ) === false) { + $context->contextual_type_resolver = $was_contextual_type_resolver; + return; } + $context->contextual_type_resolver = $was_contextual_type_resolver; + $fq_class_name = $stmt->class instanceof PhpParser\Node\Name && $stmt->class->getParts() === ['parent'] ? (string) $statements_analyzer->getFQCLN() : $fq_class_name; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 7ac4ad14a2c..9fc1c914916 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -290,7 +290,6 @@ public static function checkMethodArgs( null, true, $context, - $template_result, ) !== false; } @@ -341,10 +340,6 @@ public static function checkMethodArgs( } } - $arguments_template_result = new TemplateResult([], []); - $arguments_template_result->template_types = $template_result->template_types; - $arguments_template_result->lower_bounds = $template_result->lower_bounds; - if (ArgumentsAnalyzer::analyze( $statements_analyzer, $args, @@ -352,7 +347,6 @@ public static function checkMethodArgs( (string) $method_id, $method_storage->allow_named_arg_calls ?? true, $context, - $arguments_template_result, ) === false) { return false; } diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 6fd4448a5db..86a9961edaf 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -132,6 +132,14 @@ public static function analyze( return false; } + if ($context->contextual_type_resolver !== null) { + $inferred_type = $statements_analyzer->node_data->getType($stmt); + + if ($inferred_type !== null) { + $context->contextual_type_resolver->fillTemplateResult($inferred_type); + } + } + return true; } diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index 142955b1a25..2b715ab06a1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -6,6 +6,7 @@ use Psalm\CodeLocation; use Psalm\CodeLocation\DocblockTypeLocation; use Psalm\Context; +use Psalm\ContextualTypeResolver; use Psalm\Exception\DocblockParseException; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\ClassLikeNameOptions; @@ -141,9 +142,25 @@ public static function analyze( if ($stmt->expr) { $context->inside_return = true; + $was_contextual_type_resolver = $context->contextual_type_resolver; + + $next_contextual_type = ReturnAnalyzerContextualTypeExtractor::extract( + $codebase, + $statements_analyzer, + ); + + $context->contextual_type_resolver = $next_contextual_type !== null + ? new ContextualTypeResolver( + $next_contextual_type, + new TemplateResult([], []), + $codebase, + ) + : null; + if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) { $context->inside_return = false; $context->has_returned = true; + $context->contextual_type_resolver = $was_contextual_type_resolver; return; } @@ -181,6 +198,7 @@ public static function analyze( } $context->inside_return = false; + $context->contextual_type_resolver = $was_contextual_type_resolver; } else { $stmt_type = Type::getVoid(); } diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzerContextualTypeExtractor.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzerContextualTypeExtractor.php new file mode 100644 index 00000000000..740ac158219 --- /dev/null +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzerContextualTypeExtractor.php @@ -0,0 +1,71 @@ +getSource(); + + if ($parent_source instanceof ClosureAnalyzer && $parent_source->possibly_return_type !== null) { + return $parent_source->possibly_return_type; + } + + if ($parent_source instanceof MethodAnalyzer) { + $method_identifier = $parent_source->getMethodId(); + + // $codebase->methods->getMethodReturnType gets it by ref. + $self_class = null; + + $method_return_type = $codebase->methods->getMethodReturnType( + $method_identifier, + $self_class, + $statements_analyzer, + ); + + if ($method_return_type === null) { + return null; + } + + $class_storage = $codebase->methods->getClassLikeStorageForMethod($method_identifier); + + $found_generic_params = ClassTemplateParamCollector::collect( + $codebase, + $class_storage, + $class_storage, + $method_identifier->method_name, + null, + true, + ); + + $template_result = new TemplateResult( + $class_storage->template_types ?? [], + $found_generic_params ?? [], + ); + + return TemplateInferredTypeReplacer::replace($method_return_type, $template_result, $codebase); + } + + if ($parent_source instanceof FunctionAnalyzer) { + return $parent_source + ->getFunctionLikeStorage($statements_analyzer) + ->return_type; + } + + return null; + } +} diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index 55e80d44643..0fcd5859824 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -191,12 +191,19 @@ public function analyze( self::hoistConstants($this, $stmts, $context); } + $was_contextual_resolver = $context->contextual_type_resolver; + $context->contextual_type_resolver = null; + foreach ($stmts as $stmt) { if (self::analyzeStatement($this, $stmt, $context, $global_context) === false) { + $context->contextual_type_resolver = $was_contextual_resolver; + return false; } } + $context->contextual_type_resolver = $was_contextual_resolver; + if ($root_scope && !$context->collect_initializations && !$context->collect_mutations diff --git a/src/Psalm/Internal/LanguageServer/LanguageClient.php b/src/Psalm/Internal/LanguageServer/LanguageClient.php index 9b092510e8d..12deda4e274 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageClient.php +++ b/src/Psalm/Internal/LanguageServer/LanguageClient.php @@ -68,15 +68,21 @@ public function refreshConfiguration(): void { $capabilities = $this->server->clientCapabilities; if ($capabilities->workspace->configuration ?? false) { - $this->workspace->requestConfiguration('psalm')->onResolve(function ($error, $value): void { - if ($error) { - $this->server->logError('There was an error getting configuration'); - } else { - /** @var array $value */ - [$config] = $value; - $this->configurationRefreshed((array) $config); - } - }); + $this->workspace->requestConfiguration('psalm')->onResolve( + /** + * @param mixed $error + * @param mixed $value + */ + function ($error, $value): void { + if ($error) { + $this->server->logError('There was an error getting configuration'); + } else { + /** @var array $value */ + [$config] = $value; + $this->configurationRefreshed((array) $config); + } + }, + ); } } diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index b3898842d85..aaab6442d70 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -6,11 +6,14 @@ use PhpParser\Node\Arg; use PhpParser\Node\Expr\Variable; use Psalm\Codebase; +use Psalm\Internal\Analyzer\Statements\Expression\Call\ClassTemplateParamCollector; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Provider\NodeDataProvider; +use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TypeExpander; +use Psalm\Storage\FunctionLikeParameter; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArray; @@ -25,6 +28,7 @@ use Psalm\Type\Atomic\TTemplateParam; use UnexpectedValueException; +use function array_map; use function array_slice; use function end; use function strtolower; @@ -387,42 +391,81 @@ public static function getCallableFromAtomic( && $input_type_part->value === 'Closure' ) { return new TCallable(); - } elseif ($input_type_part instanceof TNamedObject - && $codebase->classExists($input_type_part->value) - ) { - $invoke_id = new MethodIdentifier( - $input_type_part->value, - '__invoke', - ); + } elseif ($input_type_part instanceof TNamedObject) { + return self::getCallableFromInvokable($codebase, $input_type_part); + } - if ($codebase->methods->methodExists($invoke_id)) { - $declaring_method_id = $codebase->methods->getDeclaringMethodId($invoke_id); + return null; + } - if ($declaring_method_id) { - $method_storage = $codebase->methods->getStorage($declaring_method_id); - $method_fqcln = $invoke_id->fq_class_name; - $converted_return_type = null; - if ($method_storage->return_type) { - $converted_return_type = TypeExpander::expandUnion( - $codebase, - $method_storage->return_type, - $method_fqcln, - $method_fqcln, - null, - ); - } + public static function getCallableFromInvokable( + Codebase $codebase, + TNamedObject $input_type_part + ): ?TCallable { + if (!$codebase->classExists($input_type_part->value)) { + return null; + } - return new TCallable( - 'callable', - $method_storage->params, - $converted_return_type, - $method_storage->pure, - ); - } - } + $invoke_id = new MethodIdentifier( + $input_type_part->value, + '__invoke', + ); + + if (!$codebase->methods->methodExists($invoke_id)) { + return null; } - return null; + $declaring_method_id = $codebase->methods->getDeclaringMethodId($invoke_id); + + if ($declaring_method_id === null) { + return null; + } + + $method_storage = $codebase->methods->getStorage($declaring_method_id); + + $callable = new TCallable( + 'callable', + array_map( + static fn(FunctionLikeParameter $p) => $p->type !== null + ? $p->setType(TypeExpander::expandUnion( + $codebase, + $p->type, + $invoke_id->fq_class_name, + $invoke_id->fq_class_name, + null, + )) + : $p, + $method_storage->params, + ), + $method_storage->return_type !== null + ? TypeExpander::expandUnion( + $codebase, + $method_storage->return_type, + $invoke_id->fq_class_name, + $invoke_id->fq_class_name, + null, + ) + : null, + $method_storage->pure, + ); + + $invokable_storage = $codebase->classlike_storage_provider->get($input_type_part->value); + + $template_result = new TemplateResult( + $invokable_storage->template_types ?? [], + ClassTemplateParamCollector::collect( + $codebase, + $invokable_storage, + $invokable_storage, + '__invoke', + $input_type_part, + ) ?? [], + ); + + return $callable->replaceTemplateTypesWithArgTypes( + $template_result, + $codebase, + ); } /** @return null|'not-callable'|MethodIdentifier */ diff --git a/src/Psalm/Internal/Type/TemplateContextualBoundsCollector.php b/src/Psalm/Internal/Type/TemplateContextualBoundsCollector.php new file mode 100644 index 00000000000..b1f968644d0 --- /dev/null +++ b/src/Psalm/Internal/Type/TemplateContextualBoundsCollector.php @@ -0,0 +1,189 @@ +>> */ + private array $collected_atomics = []; + + private function __construct(Codebase $codebase) + { + $this->codebase = $codebase; + } + + /** + * @return array> + */ + public static function collect(Codebase $codebase, Union $contextual_type, Union $return_type): array + { + $collector = new self($codebase); + $collector->collectUnion($contextual_type, $return_type); + + return array_map( + static fn(array $template_map): array => array_map( + static fn(array $collected_atomics): Union => TypeCombiner::combine($collected_atomics, $codebase), + $template_map, + ), + $collector->collected_atomics, + ); + } + + private function collectUnion(Union $contextual_type, Union $return_type): void + { + foreach ($contextual_type->getAtomicTypes() as $contextual_atomic) { + foreach ($return_type->getAtomicTypes() as $return_atomic) { + $this->collectAtomic($contextual_atomic, $return_atomic); + } + } + } + + private function collectAtomic(Atomic $contextual_atomic, Atomic $return_atomic): void + { + if ($return_atomic instanceof TTemplateParam) { + $this->collected_atomics[$return_atomic->param_name][$return_atomic->defining_class][] = $contextual_atomic; + } elseif ($contextual_atomic instanceof TCallable || $contextual_atomic instanceof TClosure) { + $this->collectCallable($contextual_atomic, $return_atomic); + } elseif ($contextual_atomic instanceof TArray || $contextual_atomic instanceof TIterable) { + $this->collectIterable($contextual_atomic, $return_atomic); + } elseif ($contextual_atomic instanceof TKeyedArray) { + $this->collectKeyedArray($contextual_atomic, $return_atomic); + } elseif ($contextual_atomic instanceof TGenericObject) { + $this->collectGenericObject($contextual_atomic, $return_atomic); + } + } + + /** + * @param TCallable|TClosure $contextual_atomic + */ + private function collectCallable(Atomic $contextual_atomic, Atomic $return_atomic): void + { + if ($return_atomic instanceof TNamedObject + && $return_atomic->value !== 'Closure' + && $this->codebase->classOrInterfaceExists($return_atomic->value) + && $this->codebase->methodExists($return_atomic->value . '::__invoke') + ) { + $return_atomic = CallableTypeComparator::getCallableFromInvokable( + $this->codebase, + $return_atomic, + ); + } + + if ($return_atomic instanceof TCallable || $return_atomic instanceof TClosure) { + foreach ($return_atomic->params ?? [] as $offset => $return_param) { + $contextual_param = $contextual_atomic->params[$offset] ?? null; + + if (!isset($contextual_param->type) || !isset($return_param->type)) { + continue; + } + + $this->collectUnion($contextual_param->type, $return_param->type); + } + } + } + + /** + * @param TIterable|TArray $contextual_atomic + */ + private function collectIterable(Atomic $contextual_atomic, Atomic $return_atomic): void + { + if ($return_atomic instanceof TArray || $return_atomic instanceof TIterable) { + $this->collectUnion($contextual_atomic->type_params[0], $return_atomic->type_params[0]); + $this->collectUnion($contextual_atomic->type_params[1], $return_atomic->type_params[1]); + } + } + + private function collectKeyedArray(TKeyedArray $contextual_atomic, Atomic $return_atomic): void + { + if ($return_atomic instanceof TKeyedArray + && $contextual_atomic->is_list + && $contextual_atomic->isSealed() + && $return_atomic->isGenericList() + ) { + $this->collectUnion($contextual_atomic->getGenericValueType(), $return_atomic->getGenericValueType()); + } elseif ($return_atomic instanceof TKeyedArray) { + foreach ($return_atomic->properties as $return_key => $return_property) { + if (!isset($contextual_atomic->properties[$return_key])) { + continue; + } + + $this->collectUnion($contextual_atomic->properties[$return_key], $return_property); + } + + if ($contextual_atomic->fallback_params !== null && $return_atomic->fallback_params !== null) { + $this->collectUnion($contextual_atomic->fallback_params[0], $return_atomic->fallback_params[0]); + $this->collectUnion($contextual_atomic->fallback_params[1], $return_atomic->fallback_params[1]); + } + } elseif ($return_atomic instanceof TArray) { + $this->collectUnion($contextual_atomic->getGenericKeyType(), $return_atomic->type_params[0]); + $this->collectUnion($contextual_atomic->getGenericValueType(), $return_atomic->type_params[1]); + } + } + + private function collectGenericObject(TGenericObject $contextual_atomic, Atomic $return_atomic): void + { + if ($return_atomic instanceof TGenericObject + && $this->codebase->classExists($return_atomic->value) + && $this->codebase->classExtends($return_atomic->value, $contextual_atomic->value) + ) { + $contextual_storage = $this->codebase->classlike_storage_provider->get($contextual_atomic->value); + $return_storage = $this->codebase->classlike_storage_provider->get($return_atomic->value); + + $contextual_raw_atomic = $contextual_storage->getNamedObjectAtomic(); + $return_raw_atomic = $return_storage->getNamedObjectAtomic(); + + if ($contextual_raw_atomic === null || $return_raw_atomic === null) { + return; + } + + $template_result = new TemplateResult($contextual_storage->template_types ?? [], []); + + TemplateStandinTypeReplacer::fillTemplateResult( + new Union([$contextual_raw_atomic]), + $template_result, + $this->codebase, + null, + new Union([$return_raw_atomic]), + ); + + $return_atomic = $contextual_raw_atomic->replaceTemplateTypesWithArgTypes( + $template_result, + $this->codebase, + ); + } + + if ($return_atomic instanceof TGenericObject + && $contextual_atomic->value === $return_atomic->value + ) { + foreach ($return_atomic->type_params as $offset => $return_type_param) { + $contextual_type_param = $contextual_atomic->type_params[$offset] ?? null; + + if (!isset($contextual_type_param)) { + continue; + } + + $this->collectUnion($contextual_type_param, $return_type_param); + } + } + } +} diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index e329f92b632..27314cfad90 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -56,6 +56,7 @@ class TypeExpander /** * @psalm-suppress InaccessibleProperty We just created the type * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param array $do_not_expand_template_defined_at */ public static function expandUnion( Codebase $codebase, @@ -68,7 +69,8 @@ public static function expandUnion( bool $final = false, bool $expand_generic = false, bool $expand_templates = false, - bool $throw_on_unresolvable_constant = false + bool $throw_on_unresolvable_constant = false, + array $do_not_expand_template_defined_at = [] ): Union { $new_return_type_parts = []; @@ -85,6 +87,7 @@ public static function expandUnion( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); $new_return_type_parts = [...$new_return_type_parts, ...$parts]; @@ -114,6 +117,7 @@ public static function expandUnion( /** * @param string|TNamedObject|TTemplateParam|null $static_class_type * @param-out Atomic $return_type + * @param array $do_not_expand_template_defined_at * @return non-empty-list * @psalm-suppress ConflictingReferenceConstraint, ReferenceConstraintViolation The output type is always Atomic * @psalm-suppress ComplexMethod @@ -129,7 +133,8 @@ public static function expandAtomic( bool $final = false, bool $expand_generic = false, bool $expand_templates = false, - bool $throw_on_unresolvable_constant = false + bool $throw_on_unresolvable_constant = false, + array $do_not_expand_template_defined_at = [] ): array { if ($return_type instanceof TEnumCase) { return [$return_type]; @@ -151,9 +156,11 @@ public static function expandAtomic( $parent_class, $evaluate_class_constants, $evaluate_conditional_types, + $final, $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); if ($extra_type instanceof TNamedObject && $extra_type->extra_types) { @@ -200,6 +207,7 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); if ($new_as_type instanceof TNamedObject && $new_as_type !== $return_type->as_type) { @@ -208,7 +216,9 @@ public static function expandAtomic( $new_as_type, ); } - } elseif ($return_type instanceof TTemplateParam) { + } elseif ($return_type instanceof TTemplateParam + && !isset($do_not_expand_template_defined_at[$return_type->defining_class]) + ) { $new_as_type = self::expandUnion( $codebase, $return_type->as, @@ -221,6 +231,7 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); if ($expand_templates) { @@ -316,6 +327,7 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); $recursively_fleshed_out_types = [ @@ -338,6 +350,7 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); $recursively_fleshed_out_types = [ @@ -364,6 +377,7 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); } @@ -387,6 +401,7 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); $new_value_type = reset($new_value_type); @@ -420,6 +435,7 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); $potential_ints = []; @@ -448,6 +464,7 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); } if ($return_type instanceof TList) { @@ -472,6 +489,7 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); } unset($type_param); @@ -493,6 +511,7 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); if ($property_type !== $properties[$k]) { $changed = true; @@ -515,6 +534,7 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); if ($property_type !== $fallback_params[$k]) { $changed = true; @@ -549,6 +569,7 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); } unset($property_type); @@ -574,6 +595,7 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, )); } } @@ -593,6 +615,7 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); } @@ -721,6 +744,7 @@ private static function expandNamedObject( /** * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param array $do_not_expand_template_defined_at * @return non-empty-list */ private static function expandConditional( @@ -734,7 +758,8 @@ private static function expandConditional( bool $final = false, bool $expand_generic = false, bool $expand_templates = false, - bool $throw_on_unresolvable_constant = false + bool $throw_on_unresolvable_constant = false, + array $do_not_expand_template_defined_at = [] ): array { $new_as_type = self::expandUnion( $codebase, @@ -748,6 +773,7 @@ private static function expandConditional( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); if ($evaluate_conditional_types) { @@ -767,6 +793,7 @@ private static function expandConditional( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); if (count($candidate) === 1) { @@ -790,6 +817,7 @@ private static function expandConditional( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); $if_conditional_return_types = [...$if_conditional_return_types, ...$candidate_types]; @@ -810,6 +838,7 @@ private static function expandConditional( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); $else_conditional_return_types = [...$else_conditional_return_types, ...$candidate_types]; @@ -900,6 +929,7 @@ private static function expandConditional( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ), self::expandUnion( $codebase, @@ -913,6 +943,7 @@ private static function expandConditional( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ), self::expandUnion( $codebase, @@ -926,6 +957,7 @@ private static function expandConditional( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ), ); return [$return_type]; @@ -1017,6 +1049,7 @@ private static function expandPropertiesOf( /** * @param TKeyOf|TValueOf $return_type * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param array $do_not_expand_template_defined_at * @return non-empty-list */ private static function expandKeyOfValueOf( @@ -1030,7 +1063,8 @@ private static function expandKeyOfValueOf( bool $final = false, bool $expand_generic = false, bool $expand_templates = false, - bool $throw_on_unresolvable_constant = false + bool $throw_on_unresolvable_constant = false, + array $do_not_expand_template_defined_at = [] ): array { // Expand class constants to their atomics $type_atomics = []; @@ -1048,6 +1082,7 @@ private static function expandKeyOfValueOf( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, + $do_not_expand_template_defined_at, ); $type_atomics = [...$type_atomics, ...$type_param_expanded]; continue; diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index f8564444a26..174af919bbd 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -10,6 +10,7 @@ use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\TypeAlias\ClassTypeAlias; use Psalm\Issue\CodeIssue; +use Psalm\Type\Atomic\TGenericObject; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; @@ -550,4 +551,25 @@ private function hasAttribute(string $fq_class_name): bool return false; } + + public function getNamedObjectAtomic(): ?TNamedObject + { + if ($this->is_enum || $this->is_trait) { + return null; + } + + $type_params = []; + + foreach ($this->template_types ?? [] as $param_name => $type_map) { + foreach ($type_map as $defining_class => $extends) { + $type_params[] = new Union([ + new TTemplateParam($param_name, $extends, $defining_class), + ]); + } + } + + return $type_params !== [] + ? new TGenericObject($this->name, $type_params) + : new TNamedObject($this->name); + } } diff --git a/tests/Config/PluginTest.php b/tests/Config/PluginTest.php index 40a360096ef..130fd2d3304 100644 --- a/tests/Config/PluginTest.php +++ b/tests/Config/PluginTest.php @@ -977,13 +977,8 @@ function output(array $build) {} $this->analyzeFile($file_path, new Context()); } - /** - * @psalm-suppress UnevaluatedCode - */ public function testFunctionDynamicStorageProviderHook(): void { - $this->markTestSkipped('SKIPPED-dueGenericAnon'); - require_once __DIR__ . '/Plugin/StoragePlugin.php'; $this->project_analyzer = $this->getProjectAnalyzerWithConfig(