diff --git a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php index 71d5e48014f..7ba8fdc8901 100644 --- a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php @@ -64,14 +64,28 @@ public function processNode(Node $node, Scope $scope): array $assignedValueType = $scope->getType($node); } + // The offset type (null for an append) lets method-level template types and conditional types of offsetSet() + // (e.g. `@param T[TOffset] $value`) resolve against the written key. An offset that is not accepted is reported + // by OffsetAccessAssignmentRule, so only the value is checked here. + $dimType = $arrayDimFetch->dim !== null ? $scope->getType($arrayDimFetch->dim) : null; + $acceptsValue = static function (Type $varType) use ($dimType, $assignedValueType): bool { + if (!$varType->setOffsetValueType($dimType, $assignedValueType) instanceof ErrorType) { + return true; + } + + // The write is rejected. If the offset alone is accepted, the value is what is rejected. + if (!$varType->setOffsetValueType($dimType, new MixedType()) instanceof ErrorType) { + return false; + } + + return !$varType->setOffsetValueType(new MixedType(), $assignedValueType) instanceof ErrorType; + }; + $arrayTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $arrayDimFetch->var, '', - static function (Type $varType) use ($assignedValueType): bool { - $result = $varType->setOffsetValueType(new MixedType(), $assignedValueType); - return !($result instanceof ErrorType); - }, + $acceptsValue, ); $arrayType = $arrayTypeResult->getType(); if ($arrayType instanceof ErrorType) { @@ -81,8 +95,7 @@ static function (Type $varType) use ($assignedValueType): bool { if (!$isOffsetAccessible->yes()) { return []; } - $resultType = $arrayType->setOffsetValueType(new MixedType(), $assignedValueType); - if (!$resultType instanceof ErrorType) { + if ($acceptsValue($arrayType)) { return []; } diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index b358bf41754..ab2a37323e1 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -24,6 +24,8 @@ use PHPStan\Reflection\Dummy\DummyPropertyReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProviderStaticAccessor; @@ -1563,7 +1565,7 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { if ($this->isInstanceOf(ArrayAccess::class)->yes()) { - return RecursionGuard::run($this, fn (): Type => $this->getMethod('offsetGet', new OutOfClassScope())->getOnlyVariant()->getReturnType()); + return RecursionGuard::run($this, fn (): Type => $this->selectArrayAccessVariant('offsetGet', [$offsetType])->getReturnType()); } if (!$this->isExtraOffsetAccessibleClass()->no()) { @@ -1573,6 +1575,25 @@ public function getOffsetValueType(Type $offsetType): Type return new ErrorType(); } + /** + * The variant of an ArrayAccess method (offsetGet() or offsetSet()) for an offset access, + * with its method-level template types (e.g. `@template TOffset of key-of`) resolved + * against the offset (and written value) types, as for an explicit method call. + * + * @param list $argTypes + */ + private function selectArrayAccessVariant(string $methodName, array $argTypes): ParametersAcceptor + { + $variants = $this->getMethod($methodName, new OutOfClassScope())->getVariants(); + + // Nothing to resolve, so skip the resolution on this hot path. + if (count($variants) === 1 && !ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableType($variants[0])) { + return $variants[0]; + } + + return ParametersAcceptorSelector::selectFromTypes($argTypes, $variants, false); + } + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { if ($this->isOffsetAccessible()->no()) { @@ -1580,9 +1601,13 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni } if ($this->isInstanceOf(ArrayAccess::class)->yes()) { + if ($offsetType === null) { + $offsetType = new NullType(); + } + $acceptedValueType = new NeverType(); - $acceptedOffsetType = RecursionGuard::run($this, function () use (&$acceptedValueType): Type { - $parameters = $this->getMethod('offsetSet', new OutOfClassScope())->getOnlyVariant()->getParameters(); + $acceptedOffsetType = RecursionGuard::run($this, function () use ($offsetType, $valueType, &$acceptedValueType): Type { + $parameters = $this->selectArrayAccessVariant('offsetSet', [$offsetType, $valueType])->getParameters(); if (count($parameters) < 2) { throw new ShouldNotHappenException(sprintf( 'Method %s::%s() has less than 2 parameters.', @@ -1597,10 +1622,6 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $offsetParameter->getType(); }); - if ($offsetType === null) { - $offsetType = new NullType(); - } - if ( (!$offsetType instanceof MixedType && !$acceptedOffsetType->isSuperTypeOf($offsetType)->yes()) || (!$valueType instanceof MixedType && !$acceptedValueType->isSuperTypeOf($valueType)->yes()) diff --git a/tests/PHPStan/Analyser/nsrt/bug-8549.php b/tests/PHPStan/Analyser/nsrt/bug-8549.php new file mode 100644 index 00000000000..0a37a77ef05 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8549.php @@ -0,0 +1,66 @@ += 8.0 + +namespace Bug8549Types; + +use function PHPStan\Testing\assertType; + +/** + * @template TArray of array + * @extends \ArrayAccess, value-of> + */ +interface Options extends \ArrayAccess +{ + + /** + * @param key-of $offset + */ + public function offsetExists(mixed $offset): bool; + + /** + * @template TOffset of key-of + * @param TOffset $offset + * @return TArray[TOffset] + */ + public function offsetGet(mixed $offset); + + /** + * @template TOffset of key-of + * @param TOffset $offset + * @param TArray[TOffset] $value + */ + public function offsetSet(mixed $offset, mixed $value): void; + + /** + * @template TOffset of key-of + * @param TOffset $offset + */ + public function offsetUnset(mixed $offset): void; + +} + +class Foo +{ + + /** + * @param Options $options + */ + public function read(Options $options): void + { + assertType('string', $options['name']); + assertType('int', $options['age']); + assertType('int', $options['age'] ??= 5); + assertType('string', $options['name'] ??= 'x'); + assertType('int', $options['age'] += 1); + assertType('non-falsy-string', $options['name'] .= 'x'); + } + + /** + * @param Options $options + * @param 'name'|'age' $key + */ + public function unionKey(Options $options, string $key): void + { + assertType('int|string', $options[$key]); + } + +} diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php index 5bf2e287ea1..8aad94eb013 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php @@ -54,4 +54,11 @@ public function testRuleWithNullsafeVariant(): void $this->analyse([__DIR__ . '/data/offset-access-assignop-nullsafe.php'], []); } + #[RequiresPhp('>= 8.0.0')] + public function testBug8549(): void + { + $this->checkUnions = true; + $this->analyse([__DIR__ . '/data/bug-8549.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php index aef7bca9100..e829627c528 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php @@ -235,4 +235,28 @@ public function testAppendToArrayWithPhpIntMaxKey(): void ]); } + #[RequiresPhp('>= 8.0.0')] + public function testBug8549(): void + { + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-8549.php'], [ + [ + 'Cannot assign offset \'nope\' to Bug8549\Options>.', + 64, + ], + [ + 'Cannot assign new offset to Bug8549\Options>.', + 65, + ], + [ + 'Cannot assign offset 1 to Bug8549\Options>.', + 75, + ], + [ + 'Cannot assign offset \'name\'|\'nope\' to Bug8549\Options>.', + 110, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php index f59a882a1df..6ea11443aa6 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php @@ -90,4 +90,42 @@ public function testBug10783(): void $this->analyse([__DIR__ . '/../Variables/data/bug-10783.php'], []); } + #[RequiresPhp('>= 8.0.0')] + public function testBug8549(): void + { + $this->analyse([__DIR__ . '/data/bug-8549.php'], [ + [ + 'Bug8549\Options does not accept int.', + 49, + ], + [ + 'Bug8549\Options does not accept string.', + 63, + ], + [ + 'Bug8549\Options does not accept string.', + 92, + ], + [ + 'Bug8549\Options does not accept float.', + 109, + ], + ]); + } + + #[RequiresPhp('>= 8.0.0')] + public function testAppendResolvesOffsetSetAgainstNull(): void + { + $this->analyse([__DIR__ . '/data/offset-access-value-assignment-append.php'], [ + [ + 'OffsetAccessValueAssignmentAppend\Storage does not accept string.', + 35, + ], + [ + 'OffsetAccessValueAssignmentAppend\Storage does not accept int.', + 38, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-8549.php b/tests/PHPStan/Rules/Arrays/data/bug-8549.php new file mode 100644 index 00000000000..e852babba0a --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-8549.php @@ -0,0 +1,113 @@ += 8.0 + +namespace Bug8549; + +// @link https://github.com/symfony/symfony/blob/6.2/src/Symfony/Component/OptionsResolver/Options.php + +/** + * @template TArray of array + * @extends \ArrayAccess, value-of> + */ +interface Options extends \ArrayAccess { + + /** + * @param key-of $offset + */ + public function offsetExists(mixed $offset): bool; + + /** + * @template TOffset of key-of + * @param TOffset $offset + * @return TArray[TOffset] + */ + public function offsetGet(mixed $offset); + + /** + * @template TOffset of key-of + * @param TOffset $offset + * @param TArray[TOffset] $value + */ + public function offsetSet(mixed $offset, mixed $value): void; + + /** + * @template TOffset of key-of + * @param TOffset $offset + */ + public function offsetUnset(mixed $offset): void; + +} + +class HelloWorld +{ + /** + * @param Options $options + */ + public function sayHello(Options $options): void + { + \PHPStan\dumpType($options['name']); + $options['name'] = "foo"; + $options['name'] = 1; + } +} + +class MoreCases +{ + + /** + * @param Options $options + */ + public function shape(Options $options): void + { + $options['name'] = 'foo'; + $options['age'] = 1; + $options['age'] = 'foo'; + $options['nope'] = 'foo'; + $options[] = 'foo'; + } + + /** + * @param Options> $options + */ + public function map(Options $options, mixed $value): void + { + $options['context'] = 'edit'; + $options['_fields'] = $value; + $options[1] = 'edit'; + } + +} + +class CompoundAssignments +{ + + /** + * @param Options $options + */ + public function compound(Options $options): void + { + $options['age'] ??= 5; + $options['name'] ??= 'x'; + $options['age'] += 1; + $options['name'] .= 'x'; + $options['age'] .= 'x'; + } + +} + +class UnionKeys +{ + + /** + * @param Options $options + * @param 'name'|'age' $key + * @param 'name'|'nope' $partlyUnknownKey + */ + public function union(Options $options, string $key, string $partlyUnknownKey): void + { + $options[$key] = 'foo'; + $options[$key] = 1; + $options[$key] = 1.5; + $options[$partlyUnknownKey] = 'foo'; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment-append.php b/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment-append.php new file mode 100644 index 00000000000..67d6be2d9c3 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment-append.php @@ -0,0 +1,39 @@ += 8.0 + +namespace OffsetAccessValueAssignmentAppend; + +/** @implements \ArrayAccess */ +final class Storage implements \ArrayAccess +{ + + public function offsetExists(mixed $offset): bool + { + return true; + } + + public function offsetGet(mixed $offset): mixed + { + return 1; + } + + /** + * @param int|null $offset + * @param ($offset is null ? int : string) $value + */ + public function offsetSet(mixed $offset, mixed $value): void + { + } + + public function offsetUnset(mixed $offset): void + { + } + +} + +function doFoo(Storage $storage): void +{ + $storage[] = 'str'; + $storage[] = 1; + $storage[1] = 'str'; + $storage[1] = 1; +} diff --git a/turbo-ext/src/ObjectType.cpp b/turbo-ext/src/ObjectType.cpp index 37c204f98ed..6bcac9bbeb5 100644 --- a/turbo-ext/src/ObjectType.cpp +++ b/turbo-ext/src/ObjectType.cpp @@ -281,6 +281,8 @@ enum pt_ot_callback_kind /* ... ->getIterableKeyType() / ->getIterableValueType() on top */ PT_OTC_METHOD_RETURN_ITERABLE_KEY_TYPE, PT_OTC_METHOD_RETURN_ITERABLE_VALUE_TYPE, + /* offsetGet()'s return type, resolved against [$offsetType] in arg */ + PT_OTC_OFFSET_GET_RETURN_TYPE, /* the offsetSet() parameter types: the first one returned, the second * one into ->out when asked for */ PT_OTC_OFFSET_SET_PARAMETER_TYPE, @@ -1950,8 +1952,10 @@ class ObjectType zend_long arrayAccess = thisIsInstanceOfLiteral(PT_LC("ArrayAccess")); if (UNEXPECTED(arrayAccess < 0)) return zv::Val(); if (arrayAccess == PT_TRI_YES) { - zv::Val offsetGet = zv::Val::string(PT_LC("offsetGet")); - return guardedMethodReturnType(PT_OTC_METHOD_RETURN_TYPE, offsetGet.raw()); + /* [$offsetType], what offsetGet()'s template types resolve against */ + zv::Arr offsetTypes = zv::Arr::create(1); + offsetTypes.push(zv::Ref(offsetType)); + return guarded(PT_OTC_OFFSET_GET_RETURN_TYPE, NULL, offsetTypes.raw(), NULL); } zend_long extra = isExtraOffsetAccessibleClass(); @@ -1973,6 +1977,14 @@ class ObjectType zend_long arrayAccess = thisIsInstanceOfLiteral(PT_LC("ArrayAccess")); if (UNEXPECTED(arrayAccess < 0)) return zv::Val(); if (arrayAccess == PT_TRI_YES) { + zv::Val nullOffset; + if (offsetType == NULL) { + zval nullType; + if (UNEXPECTED(!pt_null_type_new(&nullType))) return zv::Val(); + nullOffset = zv::Val::adopt(nullType); + offsetType = nullOffset.raw(); + } + /* $acceptedValueType = new NeverType(), overwritten by the closure */ zv::Val acceptedValueType = pt_type_new_never_type(); if (UNEXPECTED(acceptedValueType.isUndef())) return zv::Val(); @@ -1980,7 +1992,11 @@ class ObjectType ZVAL_NULL(&none); ZVAL_NEW_REF(&outRef, &none); zv::Val outRefVal = zv::Val::adopt(outRef); - zv::Val acceptedOffsetType = guarded(PT_OTC_OFFSET_SET_PARAMETER_TYPES, NULL, NULL, NULL, outRefVal.raw()); + /* [$offsetType, $valueType], what offsetSet()'s template types resolve against */ + zv::Arr writtenTypes = zv::Arr::create(2); + writtenTypes.push(zv::Ref(offsetType)); + writtenTypes.push(zv::Ref(valueType)); + zv::Val acceptedOffsetType = guarded(PT_OTC_OFFSET_SET_PARAMETER_TYPES, NULL, writtenTypes.raw(), NULL, outRefVal.raw()); if (UNEXPECTED(acceptedOffsetType.isUndef())) return zv::Val(); zval *out = Z_REFVAL_P(outRefVal.raw()); if (Z_TYPE_P(out) == IS_OBJECT) { @@ -1991,14 +2007,6 @@ class ObjectType return zv::Val(); } - zv::Val nullOffset; - if (offsetType == NULL) { - zval nullType; - if (UNEXPECTED(!pt_null_type_new(&nullType))) return zv::Val(); - nullOffset = zv::Val::adopt(nullType); - offsetType = nullOffset.raw(); - } - bool rejected = false; if (!zv::Ref(offsetType).instanceOf(pt_ce_mixed_type)) { zend_long isSuperType = isSuperTypeOfTrinary(acceptedOffsetType.raw(), offsetType); @@ -3335,6 +3343,56 @@ zv::Val pt_object_type_referenced_classes_callback(zval *type) #define PT_THIS ObjectType(Z_OBJ_P(ZEND_THIS)) +/* $type->getMethod($methodName, new OutOfClassScope()); UNDEF = pending + * exception */ +static zv::Val otGetMethodOutOfClass(zend_object *type, zval *methodName) +{ + zv::Val outOfClassScope = pt_type_new(PT_CLASS_OUT_OF_CLASS_SCOPE, 0, NULL); + if (UNEXPECTED(outOfClassScope.isUndef())) return zv::Val(); + zv::Args methodArgs{methodName, outOfClassScope.raw()}; + zv::Val method = (type->ce == pt_ce_object_type || pt_type_method_is(type, PT_LC("getmethod"), otGetMethod)) + ? ObjectType(type).getMethod(methodName, outOfClassScope.raw()) + : pt_type_call(type, PT_LC("getmethod"), 2, methodArgs); + if (UNEXPECTED(method.isUndef())) return zv::Val(); + if (UNEXPECTED(!zv::Ref(method.raw()).isObject())) { + zend_type_error("phpstan_turbo: getMethod() must return an object"); + return zv::Val(); + } + return method; +} + +/* ParametersAcceptorSelector::selectFromTypes($types, $type->getMethod($methodName, new OutOfClassScope())->getVariants(), false): + * the method's variant with its method-level template types resolved + * against the given argument types; UNDEF = pending exception */ +static zv::Val otSelectMethodVariant(zend_object *type, zval *methodName, zval *types) +{ + zv::Val method = otGetMethodOutOfClass(type, methodName); + if (UNEXPECTED(method.isUndef())) return zv::Val(); + zv::Val variants = pt_extended_method_reflection_call(method.raw(), PT_MR_GET_VARIANTS); + if (UNEXPECTED(variants.isUndef())) return zv::Val(); + if (UNEXPECTED(!zv::Ref(variants.raw()).isArray() || !zv::Ref(types).isArray())) { + zend_type_error("phpstan_turbo: getVariants() and the argument types must be arrays"); + return zv::Val(); + } + /* nothing to resolve, so skip the resolution on this hot path: + * if (count($variants) === 1 && !ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableType($variants[0])) return $variants[0]; */ + if (zv::ArrRef(variants.raw()).size() == 1) { + zval *only = zend_hash_index_find(Z_ARRVAL_P(variants.raw()), 0); + if (only != NULL && Z_TYPE_P(only) == IS_OBJECT) { + bool hasTemplate; + if (UNEXPECTED(!pt_parameters_acceptor_selector_has_acceptor_template_or_late_resolvable_type(only, hasTemplate))) return zv::Val(); + if (!hasTemplate) return zv::Val::copyOf(zv::Ref(only)); + } + } + zv::Val variant = pt_parameters_acceptor_selector_select_from_types(types, variants.raw(), false); + if (UNEXPECTED(variant.isUndef())) return zv::Val(); + if (UNEXPECTED(!zv::Ref(variant.raw()).isObject())) { + zend_type_error("phpstan_turbo: selectFromTypes() must return an object"); + return zv::Val(); + } + return variant; +} + /* the replay of a pt_ot_callback() closure's captured call (the arguments * are ignored, as the twins' closures ignore them); UNDEF = pending * exception */ @@ -3371,27 +3429,32 @@ static zv::Val otCallbackRun(zval *captures) if (kind == PT_OTC_METHOD_RETURN_ITERABLE_KEY_TYPE) return pt_type_op(Z_OBJ_P(returnType.raw()), PT_OP_GET_ITERABLE_KEY_TYPE, 0, NULL); return pt_type_op(Z_OBJ_P(returnType.raw()), PT_OP_GET_ITERABLE_VALUE_TYPE, 0, NULL); } + case PT_OTC_OFFSET_GET_RETURN_TYPE: { + /* ParametersAcceptorSelector::selectFromTypes([$offsetType], $this->getMethod('offsetGet', new OutOfClassScope())->getVariants(), false)->getReturnType() */ + zv::Val offsetGet = zv::Val::string(PT_LC("offsetGet")); + zv::Val variant = otSelectMethodVariant(Z_OBJ_P(object), offsetGet.raw(), arg); + if (UNEXPECTED(variant.isUndef())) return zv::Val(); + return pt_parameters_acceptor_call(variant.raw(), PT_PA_GET_RETURN_TYPE); + } case PT_OTC_OFFSET_SET_PARAMETER_TYPE: case PT_OTC_OFFSET_SET_PARAMETER_TYPES: { - /* $parameters = $this->getMethod('offsetSet', new OutOfClassScope())->getOnlyVariant()->getParameters() */ - zv::Val outOfClassScope = pt_type_new(PT_CLASS_OUT_OF_CLASS_SCOPE, 0, NULL); - if (UNEXPECTED(outOfClassScope.isUndef())) return zv::Val(); + /* $parameters = $this->getMethod('offsetSet', new OutOfClassScope())->getOnlyVariant()->getParameters(), + * or, for setOffsetValueType(), the variant selected from the written types */ zv::Val offsetSet = zv::Val::string(PT_LC("offsetSet")); - zv::Args methodArgs{offsetSet.raw(), outOfClassScope.raw()}; zend_object *type = Z_OBJ_P(object); - zv::Val method = (type->ce == pt_ce_object_type || pt_type_method_is(type, PT_LC("getmethod"), otGetMethod)) - ? ObjectType(type).getMethod(offsetSet.raw(), outOfClassScope.raw()) - : pt_type_call(type, PT_LC("getmethod"), 2, methodArgs); - if (UNEXPECTED(method.isUndef())) return zv::Val(); - if (UNEXPECTED(!zv::Ref(method.raw()).isObject())) { - zend_type_error("phpstan_turbo: getMethod() must return an object"); - return zv::Val(); - } - zv::Val variant = pt_extended_method_reflection_call(method.raw(), PT_MR_GET_ONLY_VARIANT); - if (UNEXPECTED(variant.isUndef())) return zv::Val(); - if (UNEXPECTED(!zv::Ref(variant.raw()).isObject())) { - zend_type_error("phpstan_turbo: getOnlyVariant() must return an object"); - return zv::Val(); + zv::Val variant; + if (kind == PT_OTC_OFFSET_SET_PARAMETER_TYPES) { + variant = otSelectMethodVariant(type, offsetSet.raw(), arg); + if (UNEXPECTED(variant.isUndef())) return zv::Val(); + } else { + zv::Val method = otGetMethodOutOfClass(type, offsetSet.raw()); + if (UNEXPECTED(method.isUndef())) return zv::Val(); + variant = pt_extended_method_reflection_call(method.raw(), PT_MR_GET_ONLY_VARIANT); + if (UNEXPECTED(variant.isUndef())) return zv::Val(); + if (UNEXPECTED(!zv::Ref(variant.raw()).isObject())) { + zend_type_error("phpstan_turbo: getOnlyVariant() must return an object"); + return zv::Val(); + } } zv::Val parameters = pt_parameters_acceptor_call(variant.raw(), PT_PA_GET_PARAMETERS); if (UNEXPECTED(parameters.isUndef())) return zv::Val(); diff --git a/turbo-ext/src/generated/ObjectType.h b/turbo-ext/src/generated/ObjectType.h index 1e8f8ce5980..d12cde3ee89 100644 --- a/turbo-ext/src/generated/ObjectType.h +++ b/turbo-ext/src/generated/ObjectType.h @@ -191,49 +191,52 @@ inline constexpr char strings[] = "offsetType\0" /* 2316 */ "hasOffsetValueType\0" /* 2327 */ "getOffsetValueType\0" /* 2346 */ - "valueType\0" /* 2365 */ - "unionValues\0" /* 2375 */ - "true\0" /* 2387 */ - "setOffsetValueType\0" /* 2392 */ - "setExistingOffsetValueType\0" /* 2411 */ - "unsetOffset\0" /* 2438 */ - "getEnumCases\0" /* 2450 */ - "PHPStan\\Type\\Enum\\EnumCaseObjectType\0" /* 2463 */ - "getEnumCaseObject\0" /* 2500 */ - "isCallable\0" /* 2518 */ - "getCallableParametersAcceptors\0" /* 2529 */ - "findCallableParametersAcceptors\0" /* 2560 */ - "isCloneable\0" /* 2592 */ - "isInstanceOf\0" /* 2604 */ - "subtract\0" /* 2617 */ - "getTypeWithoutSubtractedType\0" /* 2626 */ - "withoutFinalByKeywordOverride\0" /* 2655 */ - "changeSubtractedType\0" /* 2685 */ - "subtractedTypes\0" /* 2706 */ - "matchAllowedSubTypes\0" /* 2722 */ - "getSubtractedType\0" /* 2743 */ - "cb\0" /* 2761 */ - "traverse\0" /* 2764 */ - "right\0" /* 2773 */ - "traverseSimultaneously\0" /* 2779 */ - "getNakedClassReflection\0" /* 2802 */ - "getClassReflection\0" /* 2826 */ - "resolvedClassReflection\0" /* 2845 */ - "PHPStan\\Type\\ObjectType\0" /* 2869 */ - "getAncestorWithClassName\0" /* 2893 */ - "getParent\0" /* 2918 */ - "getInterfaces\0" /* 2928 */ - "typeToRemove\0" /* 2942 */ - "tryRemove\0" /* 2955 */ - "getFiniteTypes\0" /* 2965 */ - "exponent\0" /* 2980 */ - "exponentiate\0" /* 2989 */ - "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 3002 */ - "toPhpDocNode\0" /* 3041 */ - "position\0" /* 3054 */ - "PHPStan\\Type\\NativeTypePosition\0" /* 3063 */ - "toNativePhpTypeNode\0" /* 3095 */ - "hasTemplateOrLateResolvableType"; /* 3115 */ + "argTypes\0" /* 2365 */ + "PHPStan\\Reflection\\ParametersAcceptor\0" /* 2374 */ + "selectArrayAccessVariant\0" /* 2412 */ + "valueType\0" /* 2437 */ + "unionValues\0" /* 2447 */ + "true\0" /* 2459 */ + "setOffsetValueType\0" /* 2464 */ + "setExistingOffsetValueType\0" /* 2483 */ + "unsetOffset\0" /* 2510 */ + "getEnumCases\0" /* 2522 */ + "PHPStan\\Type\\Enum\\EnumCaseObjectType\0" /* 2535 */ + "getEnumCaseObject\0" /* 2572 */ + "isCallable\0" /* 2590 */ + "getCallableParametersAcceptors\0" /* 2601 */ + "findCallableParametersAcceptors\0" /* 2632 */ + "isCloneable\0" /* 2664 */ + "isInstanceOf\0" /* 2676 */ + "subtract\0" /* 2689 */ + "getTypeWithoutSubtractedType\0" /* 2698 */ + "withoutFinalByKeywordOverride\0" /* 2727 */ + "changeSubtractedType\0" /* 2757 */ + "subtractedTypes\0" /* 2778 */ + "matchAllowedSubTypes\0" /* 2794 */ + "getSubtractedType\0" /* 2815 */ + "cb\0" /* 2833 */ + "traverse\0" /* 2836 */ + "right\0" /* 2845 */ + "traverseSimultaneously\0" /* 2851 */ + "getNakedClassReflection\0" /* 2874 */ + "getClassReflection\0" /* 2898 */ + "resolvedClassReflection\0" /* 2917 */ + "PHPStan\\Type\\ObjectType\0" /* 2941 */ + "getAncestorWithClassName\0" /* 2965 */ + "getParent\0" /* 2990 */ + "getInterfaces\0" /* 3000 */ + "typeToRemove\0" /* 3014 */ + "tryRemove\0" /* 3027 */ + "getFiniteTypes\0" /* 3037 */ + "exponent\0" /* 3052 */ + "exponentiate\0" /* 3061 */ + "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 3074 */ + "toPhpDocNode\0" /* 3113 */ + "position\0" /* 3126 */ + "PHPStan\\Type\\NativeTypePosition\0" /* 3135 */ + "toNativePhpTypeNode\0" /* 3167 */ + "hasTemplateOrLateResolvableType"; /* 3187 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_STRING), /* __construct $className */ reg::packed(10, MAY_BE_NULL, 25, false, false, 43), /* __construct $subtractedType */ @@ -365,17 +368,20 @@ inline constexpr reg::PackedArg args[] = { reg::packed(111, 0, 187), /* hasOffsetValueType return */ reg::packed(2316, 0, 25), /* getOffsetValueType $offsetType */ reg::packed(111, 0, 25), /* getOffsetValueType return */ + reg::packed(1323, MAY_BE_STRING), /* selectArrayAccessVariant $methodName */ + reg::packed(2365, MAY_BE_ARRAY), /* selectArrayAccessVariant $argTypes */ + reg::packed(111, 0, 2374), /* selectArrayAccessVariant return */ reg::packed(2316, MAY_BE_NULL, 25), /* setOffsetValueType $offsetType */ - reg::packed(2365, 0, 25), /* setOffsetValueType $valueType */ - reg::packed(2375, MAY_BE_BOOL, reg::NoString, false, false, 2387), /* setOffsetValueType $unionValues */ + reg::packed(2437, 0, 25), /* setOffsetValueType $valueType */ + reg::packed(2447, MAY_BE_BOOL, reg::NoString, false, false, 2459), /* setOffsetValueType $unionValues */ reg::packed(111, 0, 25), /* setOffsetValueType return */ reg::packed(2316, 0, 25), /* setExistingOffsetValueType $offsetType */ - reg::packed(2365, 0, 25), /* setExistingOffsetValueType $valueType */ + reg::packed(2437, 0, 25), /* setExistingOffsetValueType $valueType */ reg::packed(111, 0, 25), /* setExistingOffsetValueType return */ reg::packed(2316, 0, 25), /* unsetOffset $offsetType */ reg::packed(111, 0, 25), /* unsetOffset return */ reg::packed(111, MAY_BE_ARRAY), /* getEnumCases return */ - reg::packed(111, MAY_BE_NULL, 2463), /* getEnumCaseObject return */ + reg::packed(111, MAY_BE_NULL, 2535), /* getEnumCaseObject return */ reg::packed(111, 0, 187), /* isCallable return */ reg::packed(220, 0, 226), /* getCallableParametersAcceptors $scope */ reg::packed(111, MAY_BE_ARRAY), /* getCallableParametersAcceptors return */ @@ -389,28 +395,28 @@ inline constexpr reg::PackedArg args[] = { reg::packed(111, 0, 25), /* withoutFinalByKeywordOverride return */ reg::packed(10, MAY_BE_NULL, 25), /* changeSubtractedType $subtractedType */ reg::packed(111, 0, 25), /* changeSubtractedType return */ - reg::packed(2706, MAY_BE_ARRAY), /* matchAllowedSubTypes $subtractedTypes */ + reg::packed(2778, MAY_BE_ARRAY), /* matchAllowedSubTypes $subtractedTypes */ reg::packed(111, MAY_BE_NULL, 25), /* matchAllowedSubTypes return */ reg::packed(111, MAY_BE_NULL, 25), /* getSubtractedType return */ - reg::packed(2761, MAY_BE_CALLABLE), /* traverse $cb */ + reg::packed(2833, MAY_BE_CALLABLE), /* traverse $cb */ reg::packed(111, 0, 25), /* traverse return */ - reg::packed(2773, 0, 25), /* traverseSimultaneously $right */ - reg::packed(2761, MAY_BE_CALLABLE), /* traverseSimultaneously $cb */ + reg::packed(2845, 0, 25), /* traverseSimultaneously $right */ + reg::packed(2833, MAY_BE_CALLABLE), /* traverseSimultaneously $cb */ reg::packed(111, 0, 25), /* traverseSimultaneously return */ reg::packed(111, MAY_BE_NULL, 64), /* getNakedClassReflection return */ reg::packed(111, MAY_BE_NULL, 64), /* getClassReflection return */ reg::packed(111, MAY_BE_NULL, 64), /* resolvedClassReflection return */ reg::packed(0, MAY_BE_STRING), /* getAncestorWithClassName $className */ - reg::packed(111, MAY_BE_NULL, 2869), /* getAncestorWithClassName return */ - reg::packed(111, MAY_BE_NULL, 2869), /* getParent return */ + reg::packed(111, MAY_BE_NULL, 2941), /* getAncestorWithClassName return */ + reg::packed(111, MAY_BE_NULL, 2941), /* getParent return */ reg::packed(111, MAY_BE_ARRAY), /* getInterfaces return */ - reg::packed(2942, 0, 25), /* tryRemove $typeToRemove */ + reg::packed(3014, 0, 25), /* tryRemove $typeToRemove */ reg::packed(111, MAY_BE_NULL, 25), /* tryRemove return */ reg::packed(111, MAY_BE_ARRAY), /* getFiniteTypes return */ - reg::packed(2980, 0, 25), /* exponentiate $exponent */ + reg::packed(3052, 0, 25), /* exponentiate $exponent */ reg::packed(111, 0, 25), /* exponentiate return */ - reg::packed(111, 0, 3002), /* toPhpDocNode return */ - reg::packed(3054, 0, 3063), /* toNativePhpTypeNode $position */ + reg::packed(111, 0, 3074), /* toPhpDocNode return */ + reg::packed(3126, 0, 3135), /* toNativePhpTypeNode $position */ reg::packed(111, MAY_BE_BOOL), /* hasTemplateOrLateResolvableType return */ }; using Sig = reg::Sig; @@ -506,36 +512,37 @@ inline constexpr sigtab::Sig isOffsetAccessible = { { 2277 /* isOffsetAccessible inline constexpr sigtab::Sig isOffsetAccessLegal = { { 2296 /* isOffsetAccessLegal */, 0, 125, 0, 125, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig hasOffsetValueType = { { 2327 /* hasOffsetValueType */, 1, 126, 1, 127, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getOffsetValueType = { { 2346 /* getOffsetValueType */, 1, 128, 1, 129, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig setOffsetValueType = { { 2392 /* setOffsetValueType */, 2, 130, 3, 133, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig setExistingOffsetValueType = { { 2411 /* setExistingOffsetValueType */, 2, 134, 2, 136, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig unsetOffset = { { 2438 /* unsetOffset */, 1, 137, 1, 138, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getEnumCases = { { 2450 /* getEnumCases */, 0, 139, 0, 139, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getEnumCaseObject = { { 2500 /* getEnumCaseObject */, 0, 140, 0, 140, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isCallable = { { 2518 /* isCallable */, 0, 141, 0, 141, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getCallableParametersAcceptors = { { 2529 /* getCallableParametersAcceptors */, 1, 142, 1, 143, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig findCallableParametersAcceptors = { { 2560 /* findCallableParametersAcceptors */, 0, 144, 0, 144, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isCloneable = { { 2592 /* isCloneable */, 0, 145, 0, 145, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isInstanceOf = { { 2604 /* isInstanceOf */, 1, 146, 1, 147, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig subtract = { { 2617 /* subtract */, 1, 148, 1, 149, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getTypeWithoutSubtractedType = { { 2626 /* getTypeWithoutSubtractedType */, 0, 150, 0, 150, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig withoutFinalByKeywordOverride = { { 2655 /* withoutFinalByKeywordOverride */, 0, 151, 0, 151, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig changeSubtractedType = { { 2685 /* changeSubtractedType */, 1, 152, 1, 153, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig matchAllowedSubTypes = { { 2722 /* matchAllowedSubTypes */, 1, 154, 1, 155, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getSubtractedType = { { 2743 /* getSubtractedType */, 0, 156, 0, 156, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig traverse = { { 2764 /* traverse */, 1, 157, 1, 158, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig traverseSimultaneously = { { 2779 /* traverseSimultaneously */, 2, 159, 2, 161, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getNakedClassReflection = { { 2802 /* getNakedClassReflection */, 0, 162, 0, 162, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getClassReflection = { { 2826 /* getClassReflection */, 0, 163, 0, 163, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig resolvedClassReflection = { { 2845 /* resolvedClassReflection */, 0, 164, 0, 164, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getAncestorWithClassName = { { 2893 /* getAncestorWithClassName */, 1, 165, 1, 166, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getParent = { { 2918 /* getParent */, 0, 167, 0, 167, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getInterfaces = { { 2928 /* getInterfaces */, 0, 168, 0, 168, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig tryRemove = { { 2955 /* tryRemove */, 1, 169, 1, 170, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getFiniteTypes = { { 2965 /* getFiniteTypes */, 0, 171, 0, 171, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig exponentiate = { { 2989 /* exponentiate */, 1, 172, 1, 173, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toPhpDocNode = { { 3041 /* toPhpDocNode */, 0, 174, 0, 174, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toNativePhpTypeNode = { { 3095 /* toNativePhpTypeNode */, 1, 175, 1, reg::NoArg, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig hasTemplateOrLateResolvableType = { { 3115 /* hasTemplateOrLateResolvableType */, 0, 176, 0, 176, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig selectArrayAccessVariant = { { 2412 /* selectArrayAccessVariant */, 2, 130, 2, 132, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig setOffsetValueType = { { 2464 /* setOffsetValueType */, 2, 133, 3, 136, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig setExistingOffsetValueType = { { 2483 /* setExistingOffsetValueType */, 2, 137, 2, 139, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig unsetOffset = { { 2510 /* unsetOffset */, 1, 140, 1, 141, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getEnumCases = { { 2522 /* getEnumCases */, 0, 142, 0, 142, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getEnumCaseObject = { { 2572 /* getEnumCaseObject */, 0, 143, 0, 143, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isCallable = { { 2590 /* isCallable */, 0, 144, 0, 144, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getCallableParametersAcceptors = { { 2601 /* getCallableParametersAcceptors */, 1, 145, 1, 146, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig findCallableParametersAcceptors = { { 2632 /* findCallableParametersAcceptors */, 0, 147, 0, 147, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isCloneable = { { 2664 /* isCloneable */, 0, 148, 0, 148, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isInstanceOf = { { 2676 /* isInstanceOf */, 1, 149, 1, 150, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig subtract = { { 2689 /* subtract */, 1, 151, 1, 152, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getTypeWithoutSubtractedType = { { 2698 /* getTypeWithoutSubtractedType */, 0, 153, 0, 153, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig withoutFinalByKeywordOverride = { { 2727 /* withoutFinalByKeywordOverride */, 0, 154, 0, 154, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig changeSubtractedType = { { 2757 /* changeSubtractedType */, 1, 155, 1, 156, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig matchAllowedSubTypes = { { 2794 /* matchAllowedSubTypes */, 1, 157, 1, 158, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getSubtractedType = { { 2815 /* getSubtractedType */, 0, 159, 0, 159, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig traverse = { { 2836 /* traverse */, 1, 160, 1, 161, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig traverseSimultaneously = { { 2851 /* traverseSimultaneously */, 2, 162, 2, 164, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getNakedClassReflection = { { 2874 /* getNakedClassReflection */, 0, 165, 0, 165, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getClassReflection = { { 2898 /* getClassReflection */, 0, 166, 0, 166, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig resolvedClassReflection = { { 2917 /* resolvedClassReflection */, 0, 167, 0, 167, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getAncestorWithClassName = { { 2965 /* getAncestorWithClassName */, 1, 168, 1, 169, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getParent = { { 2990 /* getParent */, 0, 170, 0, 170, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getInterfaces = { { 3000 /* getInterfaces */, 0, 171, 0, 171, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig tryRemove = { { 3027 /* tryRemove */, 1, 172, 1, 173, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getFiniteTypes = { { 3037 /* getFiniteTypes */, 0, 174, 0, 174, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig exponentiate = { { 3061 /* exponentiate */, 1, 175, 1, 176, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toPhpDocNode = { { 3113 /* toPhpDocNode */, 0, 177, 0, 177, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toNativePhpTypeNode = { { 3167 /* toNativePhpTypeNode */, 1, 178, 1, reg::NoArg, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig hasTemplateOrLateResolvableType = { { 3187 /* hasTemplateOrLateResolvableType */, 0, 179, 0, 179, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::ObjectType diff --git a/turbo-ext/tests/type-family-array-access-fixture.php b/turbo-ext/tests/type-family-array-access-fixture.php new file mode 100644 index 00000000000..7d27b1d5c0c --- /dev/null +++ b/turbo-ext/tests/type-family-array-access-fixture.php @@ -0,0 +1,95 @@ +, value-of> + */ +final class TemplatedArrayAccessFixture implements \ArrayAccess +{ + + /** + * @param key-of $offset + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + return true; + } + + /** + * @template TOffset of key-of + * @param TOffset $offset + * @return TArray[TOffset] + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + throw new \LogicException(); + } + + /** + * @template TOffset of key-of + * @param TOffset $offset + * @param TArray[TOffset] $value + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + } + + /** + * @template TOffset of key-of + * @param TOffset $offset + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + } + +} + +/** + * offsetSet() with a value type conditional on the offset: an append + * resolves it against a null offset. + * + * @implements \ArrayAccess + */ +final class ConditionalArrayAccessFixture implements \ArrayAccess +{ + + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + return true; + } + + /** + * @return ($offset is null ? int : string) + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + throw new \LogicException(); + } + + /** + * @param int|null $offset + * @param ($offset is null ? int : string) $value + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + } + + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + } + +} diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index 04bf5951fa4..f2140dbe18a 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -2598,6 +2598,51 @@ public function getSubtractedType(): ?\PHPStan\Type\Type $r['after reset getMethod'] = $attempt(static fn () => $subjects['trinary']->getMethod('yes', $outOfClassScope)); $r['after reset getEnumCases'] = $view($subjects['enum']->getEnumCases()); $r['after reset getAncestorWithClassName'] = $view((new $objectClass(\ArrayIterator::class))->getAncestorWithClassName(\Traversable::class)); + // offsetGet()/offsetSet() variants resolved against the offset and the + // written value: method-level template types (TOffset of key-of, + // TArray[TOffset]) and a conditional type on the offset, the fast path + // of a variant with nothing to resolve (ArrayObject) alongside + require_once __DIR__ . '/type-family-array-access-fixture.php'; + $nameKey = new \PHPStan\Type\Constant\ConstantStringType('name'); + $ageKey = new \PHPStan\Type\Constant\ConstantStringType('age'); + $shapeBuilder = \PHPStan\Type\Constant\ConstantArrayTypeBuilder::createEmpty(); + $shapeBuilder->setOffsetValueType($nameKey, new \PHPStan\Type\StringType()); + $shapeBuilder->setOffsetValueType($ageKey, new \PHPStan\Type\IntegerType()); + $arrayAccessSubjects = [ + 'templatedShape' => new $genericClass(\PHPStanTurboTests\TemplatedArrayAccessFixture::class, [$shapeBuilder->getArray()]), + 'templatedMap' => new $genericClass(\PHPStanTurboTests\TemplatedArrayAccessFixture::class, [new \PHPStan\Type\ArrayType(new \PHPStan\Type\StringType(), new \PHPStan\Type\MixedType())]), + 'templatedRaw' => new $objectClass(\PHPStanTurboTests\TemplatedArrayAccessFixture::class), + 'conditional' => new $objectClass(\PHPStanTurboTests\ConditionalArrayAccessFixture::class), + 'arrayObject' => new $genericClass(\ArrayObject::class, [new \PHPStan\Type\StringType(), new \PHPStan\Type\IntegerType()]), + ]; + $arrayAccessOffsets = [ + 'name' => $nameKey, + 'age' => $ageKey, + 'nope' => new \PHPStan\Type\Constant\ConstantStringType('nope'), + 'nameOrAge' => new \PHPStan\Type\UnionType([$nameKey, $ageKey]), + 'string' => new \PHPStan\Type\StringType(), + 'int1' => new \PHPStan\Type\Constant\ConstantIntegerType(1), + 'null' => new \PHPStan\Type\NullType(), + 'mixed' => new \PHPStan\Type\MixedType(), + ]; + $arrayAccessValues = [ + 'x' => new \PHPStan\Type\Constant\ConstantStringType('x'), + '1' => new \PHPStan\Type\Constant\ConstantIntegerType(1), + 'float' => new \PHPStan\Type\FloatType(), + 'mixed' => new \PHPStan\Type\MixedType(), + ]; + foreach ($arrayAccessSubjects as $subjectName => $subject) { + foreach ($arrayAccessOffsets as $offsetName => $offset) { + $r["array access $subjectName getOffsetValueType $offsetName"] = $attempt(static fn () => $subject->getOffsetValueType($offset)); + $r["array access $subjectName hasOffsetValueType $offsetName"] = $attempt(static fn () => $subject->hasOffsetValueType($offset)); + foreach ($arrayAccessValues as $valueName => $value) { + $r["array access $subjectName setOffsetValueType $offsetName $valueName"] = $attempt(static fn () => $subject->setOffsetValueType($offset, $value)); + } + } + foreach ($arrayAccessValues as $valueName => $value) { + $r["array access $subjectName setOffsetValueType append $valueName"] = $attempt(static fn () => $subject->setOffsetValueType(null, $value)); + } + } foreach ($r as $key => $value) { $observations["object $key"] = $value; }