Skip to content

Commit 282d9be

Browse files
ondrejmirtesclaude
andcommitted
Ask an unresolved template argument whether it is a subtype of a union
UnionType::isSuperTypeOf() compared an UnresolvedTemplateArgumentType member by member, so a marker standing for int|string was only maybe a subtype of BackedEnum|int|string. A closure whose inferred signature settled keeps its markers in the second pass, and a generic callee inferring its template type from that closure's parameter lost it - "Unable to resolve the template type" and a closure it then rejected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpbB99tJfUmeArX74xqvHv
1 parent 96da6f4 commit 282d9be

5 files changed

Lines changed: 44 additions & 2 deletions

File tree

‎src/Type/UnionType.php‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use PHPStan\Type\Generic\TemplateTypeMap;
3636
use PHPStan\Type\Generic\TemplateTypeVariance;
3737
use PHPStan\Type\Generic\TemplateUnionType;
38+
use PHPStan\Type\Generic\UnresolvedTemplateArgumentType;
3839
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
3940
use Throwable;
4041
use function array_diff_assoc;
@@ -334,6 +335,8 @@ public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult
334335
|| ($otherType instanceof IterableType && !$otherType instanceof TemplateIterableType)
335336
|| $otherType instanceof NeverType
336337
|| $otherType instanceof IntegerRangeType
338+
// stands for its delegate, which can itself be a union
339+
|| $otherType instanceof UnresolvedTemplateArgumentType
337340
) {
338341
return $otherType->isSubTypeOf($this);
339342
}

‎tests/PHPStan/Analyser/nsrt/closure-signature-from-usages.php‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ function takesMixed(mixed $m): void
3838
{
3939
}
4040

41+
/**
42+
* @template T of \BackedEnum|int|string
43+
* @param \Closure(T): string $cb
44+
* @return list<T>
45+
*/
46+
function takesConvertor(\Closure $cb): array
47+
{
48+
return [];
49+
}
50+
4151
function takesBareCallable(callable $cb): void
4252
{
4353
}
@@ -486,4 +496,13 @@ public function returnContextualTypingArrow(): void
486496
takesFactory($c);
487497
}
488498

499+
public function settledClosureSentToGenericTarget(): void
500+
{
501+
$convertor = static function (int|string $value): string {
502+
assertType('int|string', $value);
503+
return 'x' . $value;
504+
};
505+
assertType('list<int|string>', takesConvertor($convertor));
506+
}
507+
489508
}

‎tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,7 @@ public function testBug3660(): void
914914
]);
915915
}
916916

917+
#[RequiresPhp('>= 8.0.0')]
917918
public function testClosureSignatureFromUsages(): void
918919
{
919920
$this->analyse([__DIR__ . '/data/closure-signature-from-usages.php'], [

‎tests/PHPStan/Rules/Functions/data/closure-signature-from-usages.php‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?php
1+
<?php // lint >= 8.0
22

33
namespace ClosureSignatureFromUsagesRule;
44

@@ -36,3 +36,20 @@ function (): void {
3636
};
3737
takesIntToStringCallback($identity);
3838
};
39+
40+
/**
41+
* @template T of \BackedEnum|int|string
42+
* @param \Closure(T): string $cb
43+
* @return list<T>
44+
*/
45+
function takesConvertor(\Closure $cb): array
46+
{
47+
return [];
48+
}
49+
50+
function (): void {
51+
$convertor = static function (int|string $value): string {
52+
return 'x' . $value;
53+
};
54+
takesConvertor($convertor);
55+
};

‎turbo-ext/src/UnionType.cpp‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,9 @@ class UnionType
767767
}
768768
}
769769
if (!delegate) {
770-
delegate = instanceof_function(Z_OBJCE_P(otherType), pt_ce_never_type) || instanceof_function(Z_OBJCE_P(otherType), pt_ce_integer_range_type);
770+
delegate = instanceof_function(Z_OBJCE_P(otherType), pt_ce_never_type) || instanceof_function(Z_OBJCE_P(otherType), pt_ce_integer_range_type)
771+
// stands for its delegate, which can itself be a union
772+
|| instanceof_function(Z_OBJCE_P(otherType), pt_ce_unresolved_template_argument_type);
771773
}
772774
if (delegate) return pt_type_op(Z_OBJ_P(otherType), PT_OP_IS_SUB_TYPE_OF, 1, &selfZv);
773775

0 commit comments

Comments
 (0)