Skip to content

Commit 8b528ad

Browse files
authored
Merge pull request #11091 from weirdan/callable-and-lowercase-strings-are-coercible
2 parents 2a64d1c + b6cea6e commit 8b528ad

3 files changed

Lines changed: 60 additions & 10 deletions

File tree

src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,21 @@ public static function isContainedBy(
121121
return false;
122122
}
123123

124-
if ($input_type_part instanceof TCallableString
125-
&& (get_class($container_type_part) === TSingleLetter::class
126-
|| get_class($container_type_part) === TNonEmptyString::class
124+
if ($input_type_part instanceof TCallableString) {
125+
if (get_class($container_type_part) === TNonEmptyString::class
127126
|| get_class($container_type_part) === TNonFalsyString::class
128-
|| get_class($container_type_part) === TLowercaseString::class)
129-
) {
130-
return true;
127+
) {
128+
return true;
129+
}
130+
131+
if (get_class($container_type_part) === TLowercaseString::class
132+
|| get_class($container_type_part) === TSingleLetter::class
133+
) {
134+
if ($atomic_comparison_result) {
135+
$atomic_comparison_result->type_coerced = true;
136+
}
137+
return false;
138+
}
131139
}
132140

133141
if (($container_type_part instanceof TLowercaseString

tests/FunctionCallTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,14 @@ function foo(string $s) : void {
402402
'assertions' => [],
403403
'ignored_issues' => ['MixedAssignment', 'MixedArgument'],
404404
],
405+
'noRedundantErrorForCallableStrToLower' => [
406+
'code' => <<<'PHP'
407+
<?php
408+
/** @var callable-string */
409+
$function = "strlen";
410+
strtolower($function);
411+
PHP,
412+
],
405413
'objectLikeArrayAssignmentInConditional' => [
406414
'code' => '<?php
407415
$a = [];

tests/TypeComparatorTest.php

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Psalm\Internal\Provider\FakeFileProvider;
77
use Psalm\Internal\Provider\Providers;
88
use Psalm\Internal\RuntimeCaches;
9+
use Psalm\Internal\Type\Comparator\TypeComparisonResult;
910
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
1011
use Psalm\Internal\Type\TypeTokenizer;
1112
use Psalm\Tests\Internal\Provider\FakeParserCacheProvider;
@@ -129,6 +130,43 @@ public function testTypeDoesNotAcceptType(string $parent_type_string, string $ch
129130
);
130131
}
131132

133+
/** @dataProvider getCoercibleComparisons */
134+
public function testTypeIsCoercible(string $parent_type_string, string $child_type_string): void
135+
{
136+
$parent_type = Type::parseString($parent_type_string);
137+
$child_type = Type::parseString($child_type_string);
138+
139+
$result = new TypeComparisonResult();
140+
141+
$contained = UnionTypeComparator::isContainedBy(
142+
$this->project_analyzer->getCodebase(),
143+
$child_type,
144+
$parent_type,
145+
false,
146+
false,
147+
$result,
148+
);
149+
150+
$this->assertFalse($contained, 'Type ' . $parent_type_string . ' should not contain ' . $child_type_string);
151+
$this->assertTrue(
152+
$result->type_coerced,
153+
'Type ' . $parent_type_string . ' should be coercible into ' . $child_type_string,
154+
);
155+
}
156+
157+
/** @return iterable<string, list{string, string}> */
158+
public function getCoercibleComparisons(): iterable
159+
{
160+
yield 'callableStringIntoLowercaseString' => [
161+
'lowercase-string',
162+
'callable-string',
163+
];
164+
yield 'lowercaseStringIntoCallableString' => [
165+
'callable-string',
166+
'lowercase-string',
167+
];
168+
}
169+
132170
/**
133171
* @return array<array{string, string}>
134172
*/
@@ -155,10 +193,6 @@ public function getSuccessfulComparisons(): array
155193
'array{foo?: string}&array<string, mixed>',
156194
'array<never, never>',
157195
],
158-
'Lowercase-stringAndCallable-string' => [
159-
'lowercase-string',
160-
'callable-string',
161-
],
162196
'callableUnionAcceptsCallableUnion' => [
163197
'(callable(int,string[]): void)|(callable(int): void)',
164198
'(callable(int): void)|(callable(int,string[]): void)',

0 commit comments

Comments
 (0)