diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6f61a664de1..301f9f86803 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1678,7 +1678,7 @@ parameters: - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType - count: 8 + count: 7 path: src/Type/TypeCombinator.php - diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index e65d09719e6..7a2e6e49666 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -420,22 +420,27 @@ public static function doUnion(Type ...$types): Type $enumCaseTypes = []; $integerRangeTypes = []; for ($i = 0; $i < $typesCount; $i++) { - // a constant scalar refined by an accessory type ('Foo'&hasMethod(bar)) - // stays with the other types so it is compared against the plain - // constant scalars and other refinements of the same value - if (!$types[$i] instanceof IntersectionType && $types[$i]->isConstantScalarValue()->yes()) { + if ($types[$i]->isConstantScalarValue()->yes()) { $type = $types[$i]; $scalarTypes[get_class($type)][$type->describe(VerbosityLevel::cache())] = $type; unset($types[$i]); continue; } + // only a type containing every value of the scalar (not T of int, 1&T) + // absorbs all the constants of that scalar if ($types[$i]->isBoolean()->yes()) { - $hasGenericScalarTypes[ConstantBooleanType::class] = true; + if ($types[$i]->isSuperTypeOf(new BooleanType())->yes()) { + $hasGenericScalarTypes[ConstantBooleanType::class] = true; + } } elseif ($types[$i]->isFloat()->yes()) { - $hasGenericScalarTypes[ConstantFloatType::class] = true; + if ($types[$i]->isSuperTypeOf(new FloatType())->yes()) { + $hasGenericScalarTypes[ConstantFloatType::class] = true; + } } elseif ($types[$i]->isInteger()->yes() && !$types[$i] instanceof IntegerRangeType) { - $hasGenericScalarTypes[ConstantIntegerType::class] = true; + if ($types[$i]->isSuperTypeOf(new IntegerType())->yes()) { + $hasGenericScalarTypes[ConstantIntegerType::class] = true; + } } elseif ($types[$i]->isString()->yes() && $types[$i]->isClassString()->no() && TypeUtils::getAccessoryTypes($types[$i]) === []) { $hasGenericScalarTypes[ConstantStringType::class] = true; } else { @@ -474,6 +479,10 @@ public static function doUnion(Type ...$types): Type $types = array_merge(array_values($types), $integerRangeTypes); $typesCount = count($types); + if (isset($scalarTypes[IntersectionType::class])) { + $scalarTypes[IntersectionType::class] = self::reduceRefinedConstantScalars($scalarTypes[IntersectionType::class], $scalarTypes); + } + foreach ($scalarTypes as $classType => $scalarTypeItems) { if (isset($hasGenericScalarTypes[$classType])) { unset($scalarTypes[$classType]); @@ -703,6 +712,54 @@ private static function isAlreadyNormalized(array $alreadyNormalized, Type $a, T return false; } + /** + * Scalar buckets are compared only against the non-scalar union members, which + * is correct for plain constants (distinct values are disjoint) but not for + * constants refined by another type ('Foo'&hasMethod(bar)). Those are absorbed + * here by the plain constant of the same value, or by a less refined + * intersection of the same value. Values never need to be compared across. + * + * @param array $intersections + * @param array> $scalarTypes + * @return array + */ + private static function reduceRefinedConstantScalars(array $intersections, array $scalarTypes): array + { + $byValue = []; + foreach ($intersections as $key => $intersection) { + $constants = $intersection->getConstantScalarTypes(); + if (count($constants) !== 1) { + continue; + } + $valueKey = $constants[0]->describe(VerbosityLevel::cache()); + $plain = $scalarTypes[get_class($constants[0])][$valueKey] ?? null; + if ($plain !== null && $plain->isSuperTypeOf($intersection)->yes()) { + unset($intersections[$key]); + continue; + } + $byValue[get_class($constants[0])][$valueKey][] = $key; + } + + foreach ($byValue as $valueKeys) { + foreach ($valueKeys as $keys) { + $keysCount = count($keys); + for ($i = 0; $i < $keysCount; $i++) { + for ($j = 0; $j < $keysCount; $j++) { + if ($i === $j || !isset($intersections[$keys[$i]], $intersections[$keys[$j]])) { + continue; + } + if (!$intersections[$keys[$i]]->isSuperTypeOf($intersections[$keys[$j]])->yes()) { + continue; + } + unset($intersections[$keys[$j]]); + } + } + } + } + + return $intersections; + } + /** * @return array{Type, null}|array{null, Type}|null */ diff --git a/tests/PHPStan/Analyser/nsrt/bug-15591.php b/tests/PHPStan/Analyser/nsrt/bug-15591.php index e9549ef8a3d..0210d192997 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-15591.php +++ b/tests/PHPStan/Analyser/nsrt/bug-15591.php @@ -40,3 +40,30 @@ function testProperty(): void } assertType("'Bug15591\\\\Foo'", $class); } + +/** + * @template T + * @template TString of string + * @template TBool of bool + * @param T $x + * @param TString $s + * @param TBool $b + */ +function testRefinedConstantKeepsOtherConstants($x, $s, $b): void +{ + assertType('2|(1&T (function Bug15591\testRefinedConstantKeepsOtherConstants(), argument))', $x === 1 ? $x : 2); + assertType("'b'|('a'&TString of string (function Bug15591\\testRefinedConstantKeepsOtherConstants(), argument))", $s === 'a' ? $s : 'b'); + assertType('(TBool of bool (function Bug15591\testRefinedConstantKeepsOtherConstants(), argument)&true)|false', $b === true ? $b : false); +} + +function testAlreadyRefined(): void +{ + $class = Foo::class; + if (method_exists($class, 'm1')) { + foreach (['m2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm10', 'm11', 'm12', 'm13'] as $method) { + if (method_exists($class, $method)) { + } + } + assertType("'Bug15591\\\\Foo'&hasMethod(m1)", $class); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/union-template-scalar-constant.php b/tests/PHPStan/Analyser/nsrt/union-template-scalar-constant.php new file mode 100644 index 00000000000..71235cfcbb3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/union-template-scalar-constant.php @@ -0,0 +1,24 @@ +analyse([__DIR__ . '/data/bug-11507.php'], []); } + public function testBug6567(): void + { + $this->checkNullables = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-6567.php'], [ + [ + 'Function Bug6567\add() should return T of int but returns int.', + 11, + 'Type int is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + [ + 'Function Bug6567\testBool() should return T of bool but returns false.', + 27, + 'Type false is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + [ + 'Function Bug6567\testFloat() should return T of float but returns float.', + 36, + 'Type float is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + public function testBug15245(): void { $this->checkNullables = true; diff --git a/tests/PHPStan/Rules/Functions/data/bug-6567.php b/tests/PHPStan/Rules/Functions/data/bug-6567.php new file mode 100644 index 00000000000..6d1bf0d08e8 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6567.php @@ -0,0 +1,37 @@ + $p + */ +function test($p): void { + \PHPStan\dumpType(add($p)); +} + +/** + * @template T of bool + * @param T $p + * @return T + */ +function testBool($p) { + return false; +} + +/** + * @template T of float + * @param T $p + * @return T + */ +function testFloat($p) { + return $p + 1; +} diff --git a/turbo-ext/src/TypeCombinator.cpp b/turbo-ext/src/TypeCombinator.cpp index d5225cfa034..fa52a6fc038 100644 --- a/turbo-ext/src/TypeCombinator.cpp +++ b/turbo-ext/src/TypeCombinator.cpp @@ -322,6 +322,12 @@ static zv::Val booleanType() return adopted(pt_boolean_type_new(&raw), raw); } +static zv::Val floatType() +{ + zval raw; + return adopted(pt_float_type_new(&raw), raw); +} + static zv::Val stringType() { zval raw; @@ -1038,11 +1044,8 @@ class TypeCombinator Level cacheLevel(PT_LC("cache")); for (size_t i = 0; i < typesCount; i++) { zval *type = types[i].raw(); - zend_long isConstantScalarValue = PT_TRI_NO; - if (!isInstance(type, pt_ce_intersection_type)) { - isConstantScalarValue = callTrinary(type, PT_LC("isconstantscalarvalue")); - PT_FAIL_IF_NEG(isConstantScalarValue); - } + zend_long isConstantScalarValue = callTrinary(type, PT_LC("isconstantscalarvalue")); + PT_FAIL_IF_NEG(isConstantScalarValue); if (isConstantScalarValue == PT_TRI_YES) { zv::Val description = describe(type, cacheLevel); zend_string *key = describedKey(description); @@ -1054,17 +1057,29 @@ class TypeCombinator zend_long isBoolean = callTrinary(type, PT_LC("isboolean")); PT_FAIL_IF_NEG(isBoolean); if (isBoolean == PT_TRI_YES) { - hasGenericBoolean = true; + zv::Val generic = booleanType(); + PT_FAIL_IF_UNDEF(generic); + int covers = isSuperTypeYes(type, generic.raw()); + PT_FAIL_IF_NEG(covers); + if (covers) hasGenericBoolean = true; } else { zend_long isFloat = callTrinary(type, PT_LC("isfloat")); PT_FAIL_IF_NEG(isFloat); if (isFloat == PT_TRI_YES) { - hasGenericFloat = true; + zv::Val generic = floatType(); + PT_FAIL_IF_UNDEF(generic); + int covers = isSuperTypeYes(type, generic.raw()); + PT_FAIL_IF_NEG(covers); + if (covers) hasGenericFloat = true; } else { zend_long isInteger = callTrinary(type, PT_LC("isinteger")); PT_FAIL_IF_NEG(isInteger); if (isInteger == PT_TRI_YES && !isInstance(type, pt_ce_integer_range_type)) { - hasGenericInteger = true; + zv::Val generic = integerType(); + PT_FAIL_IF_UNDEF(generic); + int covers = isSuperTypeYes(type, generic.raw()); + PT_FAIL_IF_NEG(covers); + if (covers) hasGenericInteger = true; } else { int plainString = isPlainString(type); PT_FAIL_IF_NEG(plainString); @@ -1109,6 +1124,14 @@ class TypeCombinator } typesCount = types.size(); + for (ScalarGroup &group : scalarTypes) { + if (group.ce != pt_ce_intersection_type) continue; + zv::Val reduced = reduceRefinedConstantScalars(group.items.raw(), scalarTypes, cacheLevel); + PT_FAIL_IF_UNDEF(reduced); + group.items = zv::Arr::adoptVal(std::move(reduced)); + break; + } + for (ScalarGroup &group : scalarTypes) { zend_class_entry *classType = group.ce; if ((classType == pt_ce_constant_boolean_type && hasGenericBoolean) @@ -1264,6 +1287,73 @@ class TypeCombinator types.insert(types.begin() + (ptrdiff_t) i, std::make_move_iterator(inner.begin()), std::make_move_iterator(inner.end())); } + /* Mirrors reduceRefinedConstantScalars(): the constant-scalar + * intersections without those absorbed by the plain constant of the same + * value or by a less refined intersection of the same value */ + static zv::Val reduceRefinedConstantScalars(zval *intersections, std::vector &groups, Level &cacheLevel) + { + struct Refined + { + zend_string *key; + zv::Val type; + zend_class_entry *valueCe; + zv::Val valueDescription; + bool removed; + }; + std::vector entries; + for (zv::ArrayEntry entry : zv::ArrRef(intersections)) { + zval *intersection = entry.value().raw(); + entries.push_back({ entry.stringKeyOrNull(), copy(intersection), NULL, zv::Val(), false }); + Refined &refined = entries.back(); + zv::Val constants = call(intersection, PT_LC("getconstantscalartypes")); + PT_FAIL_IF_UNDEF(constants); + if (countOf(constants.raw()) != 1) continue; + zval *constant = indexOf(constants.raw(), 0); + if (UNEXPECTED(constant == NULL)) { + zend_throw_error(NULL, "Undefined array key 0"); + return zv::Val(); + } + zv::Val description = describe(constant, cacheLevel); + zend_string *valueKey = describedKey(description); + if (UNEXPECTED(valueKey == NULL)) return zv::Val(); + for (ScalarGroup &group : groups) { + if (group.ce != Z_OBJCE_P(constant)) continue; + zv::Ref plain = group.items.arrRef().find(valueKey); + if (plain.raw() == NULL) break; + int absorbed = isSuperTypeYes(plain.raw(), intersection); + PT_FAIL_IF_NEG(absorbed); + refined.removed = absorbed != 0; + break; + } + if (refined.removed) continue; + refined.valueCe = Z_OBJCE_P(constant); + refined.valueDescription = std::move(description); + } + + for (size_t i = 0; i < entries.size(); i++) { + if (entries[i].valueCe == NULL) continue; + for (size_t j = 0; j < entries.size(); j++) { + if (i == j || entries[i].removed || entries[j].removed) continue; + if (entries[j].valueCe != entries[i].valueCe) continue; + if (!zend_string_equals(Z_STR_P(entries[i].valueDescription.raw()), Z_STR_P(entries[j].valueDescription.raw()))) continue; + int absorbed = isSuperTypeYes(entries[i].type.raw(), entries[j].type.raw()); + PT_FAIL_IF_NEG(absorbed); + if (absorbed) entries[j].removed = true; + } + } + + zv::Arr result = zv::Arr::create((uint32_t) entries.size()); + for (Refined &refined : entries) { + if (refined.removed) continue; + if (UNEXPECTED(refined.key == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: a scalar group key must be a string"); + return zv::Val(); + } + result.set(refined.key, std::move(refined.type)); + } + return zv::Val(std::move(result)); + } + /* $scalarTypes[$ce][$key] = $type */ static void scalarGroupSet(std::vector &groups, zend_class_entry *ce, zend_string *key, zv::Val type) { diff --git a/turbo-ext/src/generated/TypeCombinator.h b/turbo-ext/src/generated/TypeCombinator.h index aaf3b79ef1d..4d4020447c3 100644 --- a/turbo-ext/src/generated/TypeCombinator.h +++ b/turbo-ext/src/generated/TypeCombinator.h @@ -47,39 +47,42 @@ inline constexpr char strings[] = "a\0" /* 263 */ "b\0" /* 265 */ "isAlreadyNormalized\0" /* 267 */ - "compareTypesInUnion\0" /* 287 */ - "getAccessoryCaseStringTypes\0" /* 307 */ - "downgradeNonFalsyStringToNonEmpty\0" /* 335 */ - "removeDecimalIntStringAccessory\0" /* 369 */ - "subtractedType\0" /* 401 */ - "unionWithSubtractedType\0" /* 416 */ - "PHPStan\\Type\\SubtractableType\0" /* 440 */ - "intersectWithSubtractedType\0" /* 470 */ - "arrayTypes\0" /* 498 */ - "processArrayAccessoryTypes\0" /* 509 */ - "findEmptyArrayNextToOversizedArray\0" /* 536 */ - "processArrayTypes\0" /* 571 */ - "optimizeConstantArrays\0" /* 589 */ - "countConstantArrayValueTypes\0" /* 612 */ - "array\0" /* 641 */ - "PHPStan\\Type\\Constant\\ConstantArrayType\0" /* 647 */ - "getConstantScalarValuesByKey\0" /* 687 */ - "oneUnrelatedKeyAllowed\0" /* 716 */ - "haveDisjointConstantScalarValues\0" /* 739 */ - "constantArrays\0" /* 772 */ - "preserveTaggedUnions\0" /* 787 */ - "reduceArrays\0" /* 808 */ - "isPlainOrBenevolentUnion\0" /* 821 */ - "PHPStan\\Type\\UnionType\0" /* 846 */ - "intersectFiniteUnions\0" /* 869 */ - "finiteUnionMembers\0" /* 891 */ - "intersect\0" /* 910 */ - "doIntersect\0" /* 920 */ - "intersectDefiniteConstantArrays\0" /* 932 */ - "PHPStan\\Type\\IntersectionType\0" /* 964 */ - "mergeIntersectionsForUnion\0" /* 994 */ - "removeFalsey\0" /* 1021 */ - "removeTruthy"; /* 1034 */ + "intersections\0" /* 287 */ + "scalarTypes\0" /* 301 */ + "reduceRefinedConstantScalars\0" /* 313 */ + "compareTypesInUnion\0" /* 342 */ + "getAccessoryCaseStringTypes\0" /* 362 */ + "downgradeNonFalsyStringToNonEmpty\0" /* 390 */ + "removeDecimalIntStringAccessory\0" /* 424 */ + "subtractedType\0" /* 456 */ + "unionWithSubtractedType\0" /* 471 */ + "PHPStan\\Type\\SubtractableType\0" /* 495 */ + "intersectWithSubtractedType\0" /* 525 */ + "arrayTypes\0" /* 553 */ + "processArrayAccessoryTypes\0" /* 564 */ + "findEmptyArrayNextToOversizedArray\0" /* 591 */ + "processArrayTypes\0" /* 626 */ + "optimizeConstantArrays\0" /* 644 */ + "countConstantArrayValueTypes\0" /* 667 */ + "array\0" /* 696 */ + "PHPStan\\Type\\Constant\\ConstantArrayType\0" /* 702 */ + "getConstantScalarValuesByKey\0" /* 742 */ + "oneUnrelatedKeyAllowed\0" /* 771 */ + "haveDisjointConstantScalarValues\0" /* 794 */ + "constantArrays\0" /* 827 */ + "preserveTaggedUnions\0" /* 842 */ + "reduceArrays\0" /* 863 */ + "isPlainOrBenevolentUnion\0" /* 876 */ + "PHPStan\\Type\\UnionType\0" /* 901 */ + "intersectFiniteUnions\0" /* 924 */ + "finiteUnionMembers\0" /* 946 */ + "intersect\0" /* 965 */ + "doIntersect\0" /* 975 */ + "intersectDefiniteConstantArrays\0" /* 987 */ + "PHPStan\\Type\\IntersectionType\0" /* 1019 */ + "mergeIntersectionsForUnion\0" /* 1049 */ + "removeFalsey\0" /* 1076 */ + "removeTruthy"; /* 1089 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_VOID), /* clearCache return */ reg::packed(12, 0, 17), /* addNull $type */ @@ -110,6 +113,9 @@ inline constexpr reg::PackedArg args[] = { reg::packed(263, 0, 17), /* isAlreadyNormalized $a */ reg::packed(265, 0, 17), /* isAlreadyNormalized $b */ reg::packed(0, MAY_BE_BOOL), /* isAlreadyNormalized return */ + reg::packed(287, MAY_BE_ARRAY), /* reduceRefinedConstantScalars $intersections */ + reg::packed(301, MAY_BE_ARRAY), /* reduceRefinedConstantScalars $scalarTypes */ + reg::packed(0, MAY_BE_ARRAY), /* reduceRefinedConstantScalars return */ reg::packed(263, 0, 17), /* compareTypesInUnion $a */ reg::packed(265, 0, 17), /* compareTypesInUnion $b */ reg::packed(0, MAY_BE_NULL | MAY_BE_ARRAY), /* compareTypesInUnion return */ @@ -120,46 +126,46 @@ inline constexpr reg::PackedArg args[] = { reg::packed(12, 0, 17), /* removeDecimalIntStringAccessory $type */ reg::packed(0, 0, 17), /* removeDecimalIntStringAccessory return */ reg::packed(12, 0, 17), /* unionWithSubtractedType $type */ - reg::packed(401, MAY_BE_NULL, 17), /* unionWithSubtractedType $subtractedType */ + reg::packed(456, MAY_BE_NULL, 17), /* unionWithSubtractedType $subtractedType */ reg::packed(0, 0, 17), /* unionWithSubtractedType return */ - reg::packed(263, 0, 440), /* intersectWithSubtractedType $a */ + reg::packed(263, 0, 495), /* intersectWithSubtractedType $a */ reg::packed(265, 0, 17), /* intersectWithSubtractedType $b */ reg::packed(0, 0, 17), /* intersectWithSubtractedType return */ - reg::packed(498, MAY_BE_ARRAY), /* processArrayAccessoryTypes $arrayTypes */ + reg::packed(553, MAY_BE_ARRAY), /* processArrayAccessoryTypes $arrayTypes */ reg::packed(0, MAY_BE_ARRAY), /* processArrayAccessoryTypes return */ - reg::packed(498, MAY_BE_ARRAY), /* findEmptyArrayNextToOversizedArray $arrayTypes */ + reg::packed(553, MAY_BE_ARRAY), /* findEmptyArrayNextToOversizedArray $arrayTypes */ reg::packed(0, MAY_BE_NULL, 17), /* findEmptyArrayNextToOversizedArray return */ - reg::packed(498, MAY_BE_ARRAY), /* processArrayTypes $arrayTypes */ + reg::packed(553, MAY_BE_ARRAY), /* processArrayTypes $arrayTypes */ reg::packed(0, MAY_BE_ARRAY), /* processArrayTypes return */ reg::packed(126, MAY_BE_ARRAY), /* optimizeConstantArrays $types */ reg::packed(0, MAY_BE_ARRAY), /* optimizeConstantArrays return */ reg::packed(126, MAY_BE_ARRAY), /* countConstantArrayValueTypes $types */ reg::packed(0, MAY_BE_LONG), /* countConstantArrayValueTypes return */ - reg::packed(641, 0, 647), /* getConstantScalarValuesByKey $array */ + reg::packed(696, 0, 702), /* getConstantScalarValuesByKey $array */ reg::packed(0, MAY_BE_NULL | MAY_BE_ARRAY), /* getConstantScalarValuesByKey return */ reg::packed(263, MAY_BE_NULL | MAY_BE_ARRAY), /* haveDisjointConstantScalarValues $a */ reg::packed(265, MAY_BE_NULL | MAY_BE_ARRAY), /* haveDisjointConstantScalarValues $b */ - reg::packed(716, MAY_BE_BOOL), /* haveDisjointConstantScalarValues $oneUnrelatedKeyAllowed */ + reg::packed(771, MAY_BE_BOOL), /* haveDisjointConstantScalarValues $oneUnrelatedKeyAllowed */ reg::packed(0, MAY_BE_BOOL), /* haveDisjointConstantScalarValues return */ - reg::packed(772, MAY_BE_ARRAY), /* reduceArrays $constantArrays */ - reg::packed(787, MAY_BE_BOOL), /* reduceArrays $preserveTaggedUnions */ + reg::packed(827, MAY_BE_ARRAY), /* reduceArrays $constantArrays */ + reg::packed(842, MAY_BE_BOOL), /* reduceArrays $preserveTaggedUnions */ reg::packed(0, MAY_BE_ARRAY), /* reduceArrays return */ reg::packed(12, 0, 17), /* isPlainOrBenevolentUnion $type */ reg::packed(0, MAY_BE_BOOL), /* isPlainOrBenevolentUnion return */ - reg::packed(263, 0, 846), /* intersectFiniteUnions $a */ - reg::packed(265, 0, 846), /* intersectFiniteUnions $b */ + reg::packed(263, 0, 901), /* intersectFiniteUnions $a */ + reg::packed(265, 0, 901), /* intersectFiniteUnions $b */ reg::packed(0, MAY_BE_NULL, 17), /* intersectFiniteUnions return */ - reg::packed(132, 0, 846), /* finiteUnionMembers $union */ + reg::packed(132, 0, 901), /* finiteUnionMembers $union */ reg::packed(0, MAY_BE_NULL | MAY_BE_ARRAY), /* finiteUnionMembers return */ reg::packed(126, 0, 17, false, true), /* intersect $types */ reg::packed(0, 0, 17), /* intersect return */ reg::packed(126, 0, 17, false, true), /* doIntersect $types */ reg::packed(0, 0, 17), /* doIntersect return */ - reg::packed(263, 0, 647), /* intersectDefiniteConstantArrays $a */ - reg::packed(265, 0, 647), /* intersectDefiniteConstantArrays $b */ + reg::packed(263, 0, 702), /* intersectDefiniteConstantArrays $a */ + reg::packed(265, 0, 702), /* intersectDefiniteConstantArrays $b */ reg::packed(0, 0, 17), /* intersectDefiniteConstantArrays return */ - reg::packed(263, 0, 964), /* mergeIntersectionsForUnion $a */ - reg::packed(265, 0, 964), /* mergeIntersectionsForUnion $b */ + reg::packed(263, 0, 1019), /* mergeIntersectionsForUnion $a */ + reg::packed(265, 0, 1019), /* mergeIntersectionsForUnion $b */ reg::packed(0, MAY_BE_NULL, 17), /* mergeIntersectionsForUnion return */ reg::packed(12, 0, 17), /* removeFalsey $type */ reg::packed(0, 0, 17), /* removeFalsey return */ @@ -183,29 +189,30 @@ inline constexpr sigtab::Sig doUnion = { { 138 /* doUnion */, 0, 17, 1, 18, ZEND inline constexpr sigtab::Sig addMarker = { { 213 /* addMarker */, 2, 19, 2, 21, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig unionWithMarkers = { { 228 /* unionWithMarkers */, 2, 22, 2, 24, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig isAlreadyNormalized = { { 267 /* isAlreadyNormalized */, 3, 25, 3, 28, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig compareTypesInUnion = { { 287 /* compareTypesInUnion */, 2, 29, 2, 31, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getAccessoryCaseStringTypes = { { 307 /* getAccessoryCaseStringTypes */, 1, 32, 1, 33, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig downgradeNonFalsyStringToNonEmpty = { { 335 /* downgradeNonFalsyStringToNonEmpty */, 1, 34, 1, 35, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig removeDecimalIntStringAccessory = { { 369 /* removeDecimalIntStringAccessory */, 1, 36, 1, 37, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig unionWithSubtractedType = { { 416 /* unionWithSubtractedType */, 2, 38, 2, 40, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersectWithSubtractedType = { { 470 /* intersectWithSubtractedType */, 2, 41, 2, 43, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig processArrayAccessoryTypes = { { 509 /* processArrayAccessoryTypes */, 1, 44, 1, 45, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig findEmptyArrayNextToOversizedArray = { { 536 /* findEmptyArrayNextToOversizedArray */, 1, 46, 1, 47, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig processArrayTypes = { { 571 /* processArrayTypes */, 1, 48, 1, 49, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig optimizeConstantArrays = { { 589 /* optimizeConstantArrays */, 1, 50, 1, 51, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig countConstantArrayValueTypes = { { 612 /* countConstantArrayValueTypes */, 1, 52, 1, 53, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getConstantScalarValuesByKey = { { 687 /* getConstantScalarValuesByKey */, 1, 54, 1, 55, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig haveDisjointConstantScalarValues = { { 739 /* haveDisjointConstantScalarValues */, 3, 56, 3, 59, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig reduceArrays = { { 808 /* reduceArrays */, 2, 60, 2, 62, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig isPlainOrBenevolentUnion = { { 821 /* isPlainOrBenevolentUnion */, 1, 63, 1, 64, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersectFiniteUnions = { { 869 /* intersectFiniteUnions */, 2, 65, 2, 67, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig finiteUnionMembers = { { 891 /* finiteUnionMembers */, 1, 68, 1, 69, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersect = { { 910 /* intersect */, 0, 70, 1, 71, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig doIntersect = { { 920 /* doIntersect */, 0, 72, 1, 73, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersectDefiniteConstantArrays = { { 932 /* intersectDefiniteConstantArrays */, 2, 74, 2, 76, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig mergeIntersectionsForUnion = { { 994 /* mergeIntersectionsForUnion */, 2, 77, 2, 79, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig removeFalsey = { { 1021 /* removeFalsey */, 1, 80, 1, 81, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig removeTruthy = { { 1034 /* removeTruthy */, 1, 82, 1, 83, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig reduceRefinedConstantScalars = { { 313 /* reduceRefinedConstantScalars */, 2, 29, 2, 31, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig compareTypesInUnion = { { 342 /* compareTypesInUnion */, 2, 32, 2, 34, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getAccessoryCaseStringTypes = { { 362 /* getAccessoryCaseStringTypes */, 1, 35, 1, 36, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig downgradeNonFalsyStringToNonEmpty = { { 390 /* downgradeNonFalsyStringToNonEmpty */, 1, 37, 1, 38, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig removeDecimalIntStringAccessory = { { 424 /* removeDecimalIntStringAccessory */, 1, 39, 1, 40, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig unionWithSubtractedType = { { 471 /* unionWithSubtractedType */, 2, 41, 2, 43, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersectWithSubtractedType = { { 525 /* intersectWithSubtractedType */, 2, 44, 2, 46, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig processArrayAccessoryTypes = { { 564 /* processArrayAccessoryTypes */, 1, 47, 1, 48, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig findEmptyArrayNextToOversizedArray = { { 591 /* findEmptyArrayNextToOversizedArray */, 1, 49, 1, 50, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig processArrayTypes = { { 626 /* processArrayTypes */, 1, 51, 1, 52, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig optimizeConstantArrays = { { 644 /* optimizeConstantArrays */, 1, 53, 1, 54, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig countConstantArrayValueTypes = { { 667 /* countConstantArrayValueTypes */, 1, 55, 1, 56, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getConstantScalarValuesByKey = { { 742 /* getConstantScalarValuesByKey */, 1, 57, 1, 58, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig haveDisjointConstantScalarValues = { { 794 /* haveDisjointConstantScalarValues */, 3, 59, 3, 62, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig reduceArrays = { { 863 /* reduceArrays */, 2, 63, 2, 65, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig isPlainOrBenevolentUnion = { { 876 /* isPlainOrBenevolentUnion */, 1, 66, 1, 67, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersectFiniteUnions = { { 924 /* intersectFiniteUnions */, 2, 68, 2, 70, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig finiteUnionMembers = { { 946 /* finiteUnionMembers */, 1, 71, 1, 72, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersect = { { 965 /* intersect */, 0, 73, 1, 74, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig doIntersect = { { 975 /* doIntersect */, 0, 75, 1, 76, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersectDefiniteConstantArrays = { { 987 /* intersectDefiniteConstantArrays */, 2, 77, 2, 79, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig mergeIntersectionsForUnion = { { 1049 /* mergeIntersectionsForUnion */, 2, 80, 2, 82, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig removeFalsey = { { 1076 /* removeFalsey */, 1, 83, 1, 84, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig removeTruthy = { { 1089 /* removeTruthy */, 1, 85, 1, 86, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::TypeCombinator diff --git a/turbo-ext/tests/initializer-escape.php b/turbo-ext/tests/initializer-escape.php index fea64a8242e..9e48cfd9499 100644 --- a/turbo-ext/tests/initializer-escape.php +++ b/turbo-ext/tests/initializer-escape.php @@ -106,7 +106,15 @@ public function resolveConcatType($left, $right) { return $this->inner->resolveC // result, which the prefix does not let through - the type callback // was kept before that, reading the type the dependencies are made of // (the native type, read below for the other handlers, is not) - check($label === 'ClassConstFetchHandler' && str_contains($e->getMessage(), 'PHPStanTurbo\\Dependencies'), "escaping getTypeCallback ($label): " . get_class($e) . ': ' . $e->getMessage()); + // the twin resolver computes `$a - 3` over the native int and its own + // constant 3: the native int is not a supertype of the twin's 3, so + // their union mixes both families, which the prefix does not let + // through either - the type callback was kept before that + check( + ($label === 'ClassConstFetchHandler' && str_contains($e->getMessage(), 'PHPStanTurbo\\Dependencies')) + || ($label === 'BinaryOpHandler' && str_contains($e->getMessage(), 'PHPStanTurbo\\TrinaryLogic returned')), + "escaping getTypeCallback ($label): " . get_class($e) . ': ' . $e->getMessage(), + ); } catch (\Throwable $e) { check(false, "escaping getTypeCallback ($label): " . get_class($e) . ': ' . $e->getMessage()); } finally { @@ -138,7 +146,7 @@ public function resolveConcatType($left, $right) { return $this->inner->resolveC unset($storage, $ieVarResult, $ieTypeCallback); gc_collect_cycles(); $ieDeepStack(200); -check(count($ieKeeping->kept) === 16, 'escaping getTypeCallback: the keeping resolver was handed the callbacks (' . count($ieKeeping->kept) . ')'); +check(count($ieKeeping->kept) === 15, 'escaping getTypeCallback: the keeping resolver was handed the callbacks (' . count($ieKeeping->kept) . ')'); foreach ($ieKeeping->kept as $i => [$method, $callback, $asked, $answered]) { $ieDeepStack(50); try {