From dae7718ae82e8417b2375ab7ed23b3370f6b201c Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Tue, 17 Jan 2017 11:17:49 -0500 Subject: [PATCH] Support array_combine types and introduce a MoreSpecificReturnType issue --- config.xsd | 1 + src/Psalm/Checker/FunctionChecker.php | 46 ++++++++++++++++++- src/Psalm/Checker/FunctionLikeChecker.php | 37 +++++++++++---- .../Statements/Expression/AssertionFinder.php | 2 +- src/Psalm/Issue/MoreSpecificReturnType.php | 6 +++ 5 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 src/Psalm/Issue/MoreSpecificReturnType.php diff --git a/config.xsd b/config.xsd index c3fb3183d..b13da1b3b 100644 --- a/config.xsd +++ b/config.xsd @@ -117,6 +117,7 @@ + diff --git a/src/Psalm/Checker/FunctionChecker.php b/src/Psalm/Checker/FunctionChecker.php index 8e318caf0..f836fc18c 100644 --- a/src/Psalm/Checker/FunctionChecker.php +++ b/src/Psalm/Checker/FunctionChecker.php @@ -405,6 +405,7 @@ class FunctionChecker extends FunctionLikeChecker } $first_arg = isset($call_args[0]->value) ? $call_args[0]->value : null; + $second_arg = isset($call_args[1]->value) ? $call_args[1]->value : null; $first_arg_array_generic = $first_arg && isset($first_arg->inferredType) @@ -493,6 +494,49 @@ class FunctionChecker extends FunctionLikeChecker return Type::getArray(); } + if ($call_map_key === 'array_combine') { + $second_arg_array_generic = $second_arg + && isset($second_arg->inferredType) + && isset($second_arg->inferredType->types['array']) + && $second_arg->inferredType->types['array'] instanceof Type\Atomic\TArray + ? $second_arg->inferredType->types['array'] + : null; + + $second_arg_array_objectlike = $second_arg + && isset($second_arg->inferredType) + && isset($second_arg->inferredType->types['array']) + && $second_arg->inferredType->types['array'] instanceof Type\Atomic\ObjectLike + ? $second_arg->inferredType->types['array'] + : null; + + if ($second_arg_array_generic || $second_arg_array_objectlike) { + if ($first_arg && isset($first_arg->inferredType) && $first_arg->inferredType->hasArray()) { + if ($first_arg_array_generic) { + $keys_inner_type = clone $first_arg_array_generic->type_params[1]; + } else { + /** @var Type\Atomic\ObjectLike $first_arg_array_objectlike */ + $keys_inner_type = $first_arg_array_objectlike->getGenericTypeParam(); + } + } else { + $keys_inner_type = Type::getMixed(); + } + + if ($second_arg_array_generic) { + $values_inner_type = clone $second_arg_array_generic->type_params[1]; + } else { + /** @var Type\Atomic\ObjectLike $second_arg_array_objectlike */ + $values_inner_type = $second_arg_array_objectlike->getGenericTypeParam(); + } + + return new Type\Union([ + new Type\Atomic\TArray([ + $keys_inner_type, + $values_inner_type + ]) + ]); + } + } + if ($call_map_key === 'array_diff') { if (!$first_arg_array_generic) { return Type::getArray(); @@ -644,7 +688,7 @@ class FunctionChecker extends FunctionLikeChecker * @return array> * @psalm-suppress MixedInferredReturnType as the use of require buggers things up * @psalm-suppress MixedAssignment - * @psalm-suppress InvalidReturnType + * @psalm-suppress MoreSpecificReturnType */ protected static function getCallMap() { diff --git a/src/Psalm/Checker/FunctionLikeChecker.php b/src/Psalm/Checker/FunctionLikeChecker.php index 3df563542..c8ffd3545 100644 --- a/src/Psalm/Checker/FunctionLikeChecker.php +++ b/src/Psalm/Checker/FunctionLikeChecker.php @@ -23,6 +23,7 @@ use Psalm\Issue\MisplacedRequiredParam; use Psalm\Issue\MissingClosureReturnType; use Psalm\Issue\MissingReturnType; use Psalm\Issue\MixedInferredReturnType; +use Psalm\Issue\MoreSpecificReturnType; use Psalm\Issue\OverriddenMethodAccess; use Psalm\IssueBuffer; use Psalm\StatementsSource; @@ -953,16 +954,34 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo return null; } - if (IssueBuffer::accepts( - new InvalidReturnType( - 'The given return type \'' . $declared_return_type . '\' for ' . $cased_method_id . - ' is incorrect, got \'' . $inferred_return_type . '\'', - $secondary_return_type_location ?: $return_type_location - ), - $this->suppressed_issues - )) { - return false; + // is the declared return type more specific than the inferred one? + if ($declared_return_type->isNullable() === $inferred_return_type->isNullable() && + TypeChecker::isContainedBy($declared_return_type, $inferred_return_type, $this->getFileChecker()) + ) { + if (IssueBuffer::accepts( + new MoreSpecificReturnType( + 'The given return type \'' . $declared_return_type . '\' for ' . $cased_method_id . + ' is more specific than the inferred return type \'' . $inferred_return_type . '\'', + $secondary_return_type_location ?: $return_type_location + ), + $this->suppressed_issues + )) { + return false; + } + } else { + if (IssueBuffer::accepts( + new InvalidReturnType( + 'The given return type \'' . $declared_return_type . '\' for ' . $cased_method_id . + ' is incorrect, got \'' . $inferred_return_type . '\'', + $secondary_return_type_location ?: $return_type_location + ), + $this->suppressed_issues + )) { + return false; + } } + + } if (!TypeChecker::hasIdenticalTypes( diff --git a/src/Psalm/Checker/Statements/Expression/AssertionFinder.php b/src/Psalm/Checker/Statements/Expression/AssertionFinder.php index fafb4d804..09836c5a3 100644 --- a/src/Psalm/Checker/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Checker/Statements/Expression/AssertionFinder.php @@ -25,7 +25,7 @@ class AssertionFinder * @param string|null $this_class_name * @param StatementsSource $source * @return array - * @psalm-suppress InvalidReturnType + * @psalm-suppress MoreSpecificReturnType */ public static function getAssertions( PhpParser\Node\Expr $conditional, diff --git a/src/Psalm/Issue/MoreSpecificReturnType.php b/src/Psalm/Issue/MoreSpecificReturnType.php new file mode 100644 index 000000000..30ec4e001 --- /dev/null +++ b/src/Psalm/Issue/MoreSpecificReturnType.php @@ -0,0 +1,6 @@ +