diff --git a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php index 40360a93b37..f422285413e 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -121,13 +121,21 @@ public static function isContainedBy( return false; } - if ($input_type_part instanceof TCallableString - && (get_class($container_type_part) === TSingleLetter::class - || get_class($container_type_part) === TNonEmptyString::class + if ($input_type_part instanceof TCallableString) { + if (get_class($container_type_part) === TNonEmptyString::class || get_class($container_type_part) === TNonFalsyString::class - || get_class($container_type_part) === TLowercaseString::class) - ) { - return true; + ) { + return true; + } + + if (get_class($container_type_part) === TLowercaseString::class + || get_class($container_type_part) === TSingleLetter::class + ) { + if ($atomic_comparison_result) { + $atomic_comparison_result->type_coerced = true; + } + return false; + } } if (($container_type_part instanceof TLowercaseString diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 833b38af431..b928a42cecf 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -402,6 +402,14 @@ function foo(string $s) : void { 'assertions' => [], 'ignored_issues' => ['MixedAssignment', 'MixedArgument'], ], + 'noRedundantErrorForCallableStrToLower' => [ + 'code' => <<<'PHP' + [ 'code' => 'project_analyzer->getCodebase(), + $child_type, + $parent_type, + false, + false, + $result, + ); + + $this->assertFalse($contained, 'Type ' . $parent_type_string . ' should not contain ' . $child_type_string); + $this->assertTrue( + $result->type_coerced, + 'Type ' . $parent_type_string . ' should be coercible into ' . $child_type_string, + ); + } + + /** @return iterable */ + public function getCoercibleComparisons(): iterable + { + yield 'callableStringIntoLowercaseString' => [ + 'lowercase-string', + 'callable-string', + ]; + yield 'lowercaseStringIntoCallableString' => [ + 'callable-string', + 'lowercase-string', + ]; + } + /** * @return array */ @@ -155,10 +193,6 @@ public function getSuccessfulComparisons(): array 'array{foo?: string}&array', 'array', ], - 'Lowercase-stringAndCallable-string' => [ - 'lowercase-string', - 'callable-string', - ], 'callableUnionAcceptsCallableUnion' => [ '(callable(int,string[]): void)|(callable(int): void)', '(callable(int): void)|(callable(int,string[]): void)',