Skip to content

Commit 044a41b

Browse files
committed
Allow Traversable in PHPDoc and Iterator in native type to cooperate
1 parent 807bca9 commit 044a41b

3 files changed

Lines changed: 62 additions & 3 deletions

File tree

‎phpstan-baseline.neon‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1723,6 +1723,12 @@ parameters:
17231723
count: 1
17241724
path: src/Type/TypehintHelper.php
17251725

1726+
-
1727+
rawMessage: 'Doing instanceof PHPStan\Type\TypeWithClassName is error-prone and deprecated. Use Type::getObjectClassNames() or Type::getObjectClassReflections() instead.'
1728+
identifier: phpstanApi.instanceofType
1729+
count: 1
1730+
path: src/Type/TypehintHelper.php
1731+
17261732
-
17271733
rawMessage: 'Doing instanceof PHPStan\Type\CallableType is error-prone and deprecated. Use Type::isCallable() and Type::getCallableParametersAcceptors() instead.'
17281734
identifier: phpstanApi.instanceofType

‎src/Type/TypehintHelper.php‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Type\Constant\ConstantArrayType;
1414
use PHPStan\Type\Generic\TemplateTypeHelper;
1515
use ReflectionType;
16+
use Traversable;
1617
use function array_map;
1718
use function count;
1819
use function get_class;
@@ -126,11 +127,26 @@ public static function decideType(
126127
}
127128
}
128129

130+
$resolvedPhpDocTypeToBounds = TemplateTypeHelper::resolveToBounds($phpDocType);
131+
$isSuperType = $type->isSuperTypeOf($resolvedPhpDocTypeToBounds);
132+
if (
133+
!$isSuperType->yes()
134+
&& (new ObjectType(Traversable::class))->isSuperTypeOf($phpDocType)->yes()
135+
&& $type instanceof TypeWithClassName
136+
) {
137+
// if native type is Iterator and PHPDoc type is Traversable
138+
// Allow PHPDoc type to win
139+
$traversableAncestor = $type->getAncestorWithClassName(Traversable::class);
140+
if ($traversableAncestor !== null) {
141+
$isSuperType = $traversableAncestor->isSuperTypeOf($resolvedPhpDocTypeToBounds);
142+
}
143+
}
144+
129145
if (
130146
($type->isCallable()->yes() && $phpDocType->isCallable()->yes())
131147
|| (
132148
(!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed()))
133-
&& $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes()
149+
&& $isSuperType->yes()
134150
)
135151
) {
136152
$resultType = $phpDocType;

‎turbo-ext/src/TypehintHelper.cpp‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,12 @@ class TypehintHelper
357357

358358
/* ($type->isCallable()->yes() && $phpDocType->isCallable()->yes())
359359
* || ((!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed()))
360-
* && $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes())
361-
* — short-circuited as the twin evaluates it; false = pending exception */
360+
* && $isSuperType->yes()), where $isSuperType is
361+
* $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType)),
362+
* or for a native TypeWithClassName below a Traversable PHPDoc type its
363+
* Traversable ancestor's verdict — the twin computes $isSuperType up
364+
* front, here it is computed where it decides (pure calls); false =
365+
* pending exception */
362366
[[nodiscard]] static bool decidesForPhpDoc(zval *type, zval *phpDocType, bool &out)
363367
{
364368
zend_long typeCallable = pt_type_op_trinary(Z_OBJ_P(type), PT_OP_IS_CALLABLE, 0, NULL);
@@ -392,6 +396,39 @@ class TypehintHelper
392396
if (UNEXPECTED(isSuperType.isUndef())) return false;
393397
zend_long trinary = pt_type_result_trinary(isSuperType.raw());
394398
if (UNEXPECTED(trinary < 0)) return false;
399+
if (trinary != PT_TRI_YES) {
400+
/* if native type is Iterator and PHPDoc type is Traversable,
401+
* allow PHPDoc type to win:
402+
* (new ObjectType(Traversable::class))->isSuperTypeOf($phpDocType)->yes()
403+
* && $type instanceof TypeWithClassName */
404+
zval traversableRaw;
405+
zend_string *traversableName = zend_string_init("Traversable", sizeof("Traversable") - 1, 0);
406+
bool created = pt_object_type_new(&traversableRaw, traversableName);
407+
zend_string_release(traversableName);
408+
if (UNEXPECTED(!created)) return false;
409+
zv::Val traversableType = zv::Val::adopt(traversableRaw);
410+
zv::Val phpDocTraversable = pt_type_op(Z_OBJ_P(traversableType.raw()), PT_OP_IS_SUPER_TYPE_OF, 1, phpDocType);
411+
if (UNEXPECTED(phpDocTraversable.isUndef())) return false;
412+
zend_long phpDocIsTraversable = pt_type_result_trinary(phpDocTraversable.raw());
413+
if (UNEXPECTED(phpDocIsTraversable < 0)) return false;
414+
if (phpDocIsTraversable == PT_TRI_YES) {
415+
bool withClassName;
416+
if (UNEXPECTED(!pt_type_instanceof(type, PT_CLASS_TYPE_WITH_CLASS_NAME, withClassName))) return false;
417+
if (withClassName) {
418+
zval className;
419+
ZVAL_STRINGL(&className, "Traversable", sizeof("Traversable") - 1);
420+
zv::Val ancestor = pt_type_call(Z_OBJ_P(type), PT_LC("getancestorwithclassname"), 1, &className);
421+
zval_ptr_dtor(&className);
422+
if (UNEXPECTED(ancestor.isUndef())) return false;
423+
if (!ancestor.isNull()) {
424+
zv::Val ancestorIsSuperType = pt_type_op(Z_OBJ_P(ancestor.raw()), PT_OP_IS_SUPER_TYPE_OF, 1, bounds.raw());
425+
if (UNEXPECTED(ancestorIsSuperType.isUndef())) return false;
426+
trinary = pt_type_result_trinary(ancestorIsSuperType.raw());
427+
if (UNEXPECTED(trinary < 0)) return false;
428+
}
429+
}
430+
}
431+
}
395432
out = trinary == PT_TRI_YES;
396433
return true;
397434
}

0 commit comments

Comments
 (0)