From ba8f306eabde0c7ad3423d5709cd962294fa6dcf Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 1 Nov 2026 13:29:34 -0800 Subject: [PATCH 1/8] Resolve offsetSet() template types for ArrayAccess offset writes An offset write `$object[$offset] = $value` was checked against the parameters of offsetSet() taken from getOnlyVariant(), so method-level template types were never resolved. With an offsetSet() declared as @template TOffset of key-of @param TOffset $offset @param T[TOffset] $value the accepted offset type stayed the unresolved TOffset and the accepted value type T[TOffset], so every write was rejected, including valid ones. The explicit $object->offsetSet($offset, $value) call was already accepted, since a method call resolves the templates from its arguments. ObjectType::setOffsetValueType() now selects the offsetSet() variant from the written offset and value types, as a call does. OffsetAccessValueAssignmentRule passes the written offset type, not mixed, so T[TOffset] resolves to the value type for that key. When the offset itself is rejected, the rule falls back to mixed, since OffsetAccessAssignmentRule already reports that offset. The native ObjectType in turbo-ext gets the same change. Co-Authored-By: Claude Opus 5.5 (medium effort) Claude-Code-Session-Id: 781e7324-e13c-4956-a86a-9047e742d5b9 --- .../OffsetAccessValueAssignmentRule.php | 18 ++++- src/Type/ObjectType.php | 19 +++-- .../Arrays/OffsetAccessAssignmentRuleTest.php | 20 +++++ .../OffsetAccessValueAssignmentRuleTest.php | 15 ++++ tests/PHPStan/Rules/Arrays/data/bug-8549.php | 78 +++++++++++++++++++ turbo-ext/src/ObjectType.cpp | 53 +++++++++---- 6 files changed, 179 insertions(+), 24 deletions(-) create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-8549.php diff --git a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php index 71d5e48014f..c35693fb6b6 100644 --- a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php @@ -64,12 +64,24 @@ public function processNode(Node $node, Scope $scope): array $assignedValueType = $scope->getType($node); } + // The offset type lets method-level template 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; + $offsetTypeFor = static function (Type $varType) use ($dimType): Type { + if ($dimType === null || $varType->setOffsetValueType($dimType, new MixedType()) instanceof ErrorType) { + return new MixedType(); + } + + return $dimType; + }; + $arrayTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $arrayDimFetch->var, '', - static function (Type $varType) use ($assignedValueType): bool { - $result = $varType->setOffsetValueType(new MixedType(), $assignedValueType); + static function (Type $varType) use ($assignedValueType, $offsetTypeFor): bool { + $result = $varType->setOffsetValueType($offsetTypeFor($varType), $assignedValueType); return !($result instanceof ErrorType); }, ); @@ -81,7 +93,7 @@ static function (Type $varType) use ($assignedValueType): bool { if (!$isOffsetAccessible->yes()) { return []; } - $resultType = $arrayType->setOffsetValueType(new MixedType(), $assignedValueType); + $resultType = $arrayType->setOffsetValueType($offsetTypeFor($arrayType), $assignedValueType); if (!$resultType instanceof ErrorType) { return []; } diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index b358bf41754..4e20f0d2cc1 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -24,6 +24,7 @@ use PHPStan\Reflection\Dummy\DummyPropertyReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProviderStaticAccessor; @@ -1580,9 +1581,19 @@ 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 { + // Resolve offsetSet()'s method-level template types (e.g. `@template TOffset of key-of`) + // against the written offset and value, as for an explicit $object->offsetSet($offset, $value) call. + $parameters = ParametersAcceptorSelector::selectFromTypes( + [$offsetType, $valueType], + $this->getMethod('offsetSet', new OutOfClassScope())->getVariants(), + false, + )->getParameters(); if (count($parameters) < 2) { throw new ShouldNotHappenException(sprintf( 'Method %s::%s() has less than 2 parameters.', @@ -1597,10 +1608,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/Rules/Arrays/OffsetAccessAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php index aef7bca9100..4216b264b12 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php @@ -235,4 +235,24 @@ 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, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php index f59a882a1df..dd457d19338 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php @@ -90,4 +90,19 @@ 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, + ], + ]); + } + } 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..763a6593f6c --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-8549.php @@ -0,0 +1,78 @@ += 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'; + } + +} diff --git a/turbo-ext/src/ObjectType.cpp b/turbo-ext/src/ObjectType.cpp index 37c204f98ed..868d6e0837d 100644 --- a/turbo-ext/src/ObjectType.cpp +++ b/turbo-ext/src/ObjectType.cpp @@ -1973,6 +1973,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 +1988,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 +2003,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); @@ -3373,7 +3377,8 @@ static zv::Val otCallbackRun(zval *captures) } case PT_OTC_OFFSET_SET_PARAMETER_TYPE: case PT_OTC_OFFSET_SET_PARAMETER_TYPES: { - /* $parameters = $this->getMethod('offsetSet', new OutOfClassScope())->getOnlyVariant()->getParameters() */ + /* $parameters = $this->getMethod('offsetSet', new OutOfClassScope())->getOnlyVariant()->getParameters(), + * or, for setOffsetValueType(), the variant selected from the written types */ zv::Val outOfClassScope = pt_type_new(PT_CLASS_OUT_OF_CLASS_SCOPE, 0, NULL); if (UNEXPECTED(outOfClassScope.isUndef())) return zv::Val(); zv::Val offsetSet = zv::Val::string(PT_LC("offsetSet")); @@ -3387,11 +3392,29 @@ static zv::Val otCallbackRun(zval *captures) 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) { + /* ParametersAcceptorSelector::selectFromTypes([$offsetType, $valueType], $method->getVariants(), false) + * resolves offsetSet()'s method-level template types against the written offset and value */ + 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(arg).isArray())) { + zend_type_error("phpstan_turbo: getVariants() and the written types must be arrays"); + return zv::Val(); + } + variant = pt_parameters_acceptor_selector_select_from_types(arg, 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(); + } + } else { + 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(); From 30aa01d56515889b3c5958f00b56084eee6c18e3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 1 Nov 2026 22:46:17 -0800 Subject: [PATCH 2/8] Resolve offsetGet() template types in ObjectType::getOffsetValueType() A plain read `$object[$offset]` is analysed as a simulated offsetGet() call, so a method-level `@template TOffset of key-of` with `@return T[TOffset]` already resolved for it. Compound assignments (`??=`, `+=`, `.=`) read the current value through ObjectType::getOffsetValueType() instead, which took offsetGet()'s return type from getOnlyVariant() with TOffset unresolved. For `Options`, `$options['age'] ??= 5` read the value as `int|string` and reported it as not accepted, and `$options['age'] += 1` was inferred as an error type. getOffsetValueType() now selects the offsetGet() variant from the offset type, as setOffsetValueType() does for offsetSet(). In the native ObjectType, the getMethod() lookup and the variant selection move into helpers that the offsetGet() and offsetSet() paths share. Co-Authored-By: Claude Opus 5.5 (high effort) Claude-Code-Session-Id: 781e7324-e13c-4956-a86a-9047e742d5b9 --- src/Type/ObjectType.php | 8 +- tests/PHPStan/Analyser/nsrt/bug-8549.php | 66 +++++++++++++++ .../Arrays/OffsetAccessAssignOpRuleTest.php | 7 ++ .../OffsetAccessValueAssignmentRuleTest.php | 4 + tests/PHPStan/Rules/Arrays/data/bug-8549.php | 17 ++++ turbo-ext/src/ObjectType.cpp | 82 +++++++++++++------ 6 files changed, 157 insertions(+), 27 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-8549.php diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 4e20f0d2cc1..557d041fa13 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -1564,7 +1564,13 @@ 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()); + // Resolve offsetGet()'s method-level template types (e.g. `@template TOffset of key-of`) + // against the offset, as for an explicit $object->offsetGet($offset) call. + return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectFromTypes( + [$offsetType], + $this->getMethod('offsetGet', new OutOfClassScope())->getVariants(), + false, + )->getReturnType()); } if (!$this->isExtraOffsetAccessibleClass()->no()) { 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/OffsetAccessValueAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php index dd457d19338..7e3a0d7388b 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php @@ -102,6 +102,10 @@ public function testBug8549(): void 'Bug8549\Options does not accept string.', 63, ], + [ + 'Bug8549\Options does not accept string.', + 92, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-8549.php b/tests/PHPStan/Rules/Arrays/data/bug-8549.php index 763a6593f6c..02401e2700d 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-8549.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-8549.php @@ -76,3 +76,20 @@ public function map(Options $options, mixed $value): void } } + +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'; + } + +} diff --git a/turbo-ext/src/ObjectType.cpp b/turbo-ext/src/ObjectType.cpp index 868d6e0837d..b267385e515 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(); @@ -3339,6 +3343,46 @@ 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(); + } + 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 */ @@ -3375,40 +3419,26 @@ 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(), * or, for setOffsetValueType(), the variant selected from the written types */ - zv::Val outOfClassScope = pt_type_new(PT_CLASS_OUT_OF_CLASS_SCOPE, 0, NULL); - if (UNEXPECTED(outOfClassScope.isUndef())) return zv::Val(); 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; if (kind == PT_OTC_OFFSET_SET_PARAMETER_TYPES) { - /* ParametersAcceptorSelector::selectFromTypes([$offsetType, $valueType], $method->getVariants(), false) - * resolves offsetSet()'s method-level template types against the written offset and value */ - 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(arg).isArray())) { - zend_type_error("phpstan_turbo: getVariants() and the written types must be arrays"); - return zv::Val(); - } - variant = pt_parameters_acceptor_selector_select_from_types(arg, variants.raw(), false); + variant = otSelectMethodVariant(type, offsetSet.raw(), arg); 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(); - } } 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())) { From ef1e05ee4475c2ca51881c53192e9ce07b6f426f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 1 Nov 2026 22:52:36 -0800 Subject: [PATCH 3/8] Skip ArrayAccess variant resolution when there is nothing to resolve Resolving the offsetGet()/offsetSet() variant against the offset and value types runs on every ArrayAccess read in a compound assignment and on every ArrayAccess write. Most ArrayAccess classes (ArrayObject, collections, option bags) declare no method-level template types, so the resolution only allocated a resolved variant equal to the original. When the method has a single variant without template or late-resolvable types, it is now used as is, as before. On a file of 6,000 writes and compound assignments to an ArrayObject and a plain ArrayAccess class, analysis took about 15% more user CPU time than before the resolution was added. It now takes about 2% more, which is within the noise of that measurement. The native ObjectType gets the same fast path. Co-Authored-By: Claude Opus 5.5 (high effort) Claude-Code-Session-Id: 781e7324-e13c-4956-a86a-9047e742d5b9 --- src/Type/ObjectType.php | 36 ++++++++++++++++++++++-------------- turbo-ext/src/ObjectType.cpp | 10 ++++++++++ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 557d041fa13..ab2a37323e1 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -24,6 +24,7 @@ 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; @@ -1564,13 +1565,7 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { if ($this->isInstanceOf(ArrayAccess::class)->yes()) { - // Resolve offsetGet()'s method-level template types (e.g. `@template TOffset of key-of`) - // against the offset, as for an explicit $object->offsetGet($offset) call. - return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectFromTypes( - [$offsetType], - $this->getMethod('offsetGet', new OutOfClassScope())->getVariants(), - false, - )->getReturnType()); + return RecursionGuard::run($this, fn (): Type => $this->selectArrayAccessVariant('offsetGet', [$offsetType])->getReturnType()); } if (!$this->isExtraOffsetAccessibleClass()->no()) { @@ -1580,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()) { @@ -1593,13 +1607,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni $acceptedValueType = new NeverType(); $acceptedOffsetType = RecursionGuard::run($this, function () use ($offsetType, $valueType, &$acceptedValueType): Type { - // Resolve offsetSet()'s method-level template types (e.g. `@template TOffset of key-of`) - // against the written offset and value, as for an explicit $object->offsetSet($offset, $value) call. - $parameters = ParametersAcceptorSelector::selectFromTypes( - [$offsetType, $valueType], - $this->getMethod('offsetSet', new OutOfClassScope())->getVariants(), - false, - )->getParameters(); + $parameters = $this->selectArrayAccessVariant('offsetSet', [$offsetType, $valueType])->getParameters(); if (count($parameters) < 2) { throw new ShouldNotHappenException(sprintf( 'Method %s::%s() has less than 2 parameters.', diff --git a/turbo-ext/src/ObjectType.cpp b/turbo-ext/src/ObjectType.cpp index b267385e515..6bcac9bbeb5 100644 --- a/turbo-ext/src/ObjectType.cpp +++ b/turbo-ext/src/ObjectType.cpp @@ -3374,6 +3374,16 @@ static zv::Val otSelectMethodVariant(zend_object *type, zval *methodName, zval * 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())) { From c114ec0d1ec7e1d951d88bd5c48e4e97daf05b48 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 1 Nov 2026 22:52:42 -0800 Subject: [PATCH 4/8] Check the offset only once a value write is rejected OffsetAccessValueAssignmentRule probed every write with setOffsetValueType($dimType, mixed) to decide which offset type to check the value against, and then called setOffsetValueType() again with the value. It now checks the write with the written offset first, and only if that is rejected does it probe the offset alone, falling back to a mixed offset when the offset itself is rejected. The outcome is the same, but an accepted write costs one setOffsetValueType() call instead of two. Co-Authored-By: Claude Opus 5.5 (high effort) Claude-Code-Session-Id: 781e7324-e13c-4956-a86a-9047e742d5b9 --- .../OffsetAccessValueAssignmentRule.php | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php index c35693fb6b6..54f191f029e 100644 --- a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php @@ -68,22 +68,26 @@ public function processNode(Node $node, Scope $scope): array // 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; - $offsetTypeFor = static function (Type $varType) use ($dimType): Type { - if ($dimType === null || $varType->setOffsetValueType($dimType, new MixedType()) instanceof ErrorType) { - return new MixedType(); + $acceptsValue = static function (Type $varType) use ($dimType, $assignedValueType): bool { + if ($dimType !== null) { + 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 $dimType; + return !$varType->setOffsetValueType(new MixedType(), $assignedValueType) instanceof ErrorType; }; $arrayTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $arrayDimFetch->var, '', - static function (Type $varType) use ($assignedValueType, $offsetTypeFor): bool { - $result = $varType->setOffsetValueType($offsetTypeFor($varType), $assignedValueType); - return !($result instanceof ErrorType); - }, + $acceptsValue, ); $arrayType = $arrayTypeResult->getType(); if ($arrayType instanceof ErrorType) { @@ -93,8 +97,7 @@ static function (Type $varType) use ($assignedValueType, $offsetTypeFor): bool { if (!$isOffsetAccessible->yes()) { return []; } - $resultType = $arrayType->setOffsetValueType($offsetTypeFor($arrayType), $assignedValueType); - if (!$resultType instanceof ErrorType) { + if ($acceptsValue($arrayType)) { return []; } From 34ea9953d1f0a70a60f42486aff30d2f9a31e943 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 1 Nov 2026 22:54:22 -0800 Subject: [PATCH 5/8] Test ArrayAccess writes with a union offset With a union offset such as 'name'|'age', TOffset resolves to the union and T[TOffset] to the union of the value types for those keys, so a value of either key's type is accepted and a value of neither is reported. An offset that is only partly known, such as 'name'|'nope', is reported once by OffsetAccessAssignmentRule, and OffsetAccessValueAssignmentRule does not report its value as well. Co-Authored-By: Claude Opus 5.5 (high effort) Claude-Code-Session-Id: 781e7324-e13c-4956-a86a-9047e742d5b9 --- .../Arrays/OffsetAccessAssignmentRuleTest.php | 4 ++++ .../OffsetAccessValueAssignmentRuleTest.php | 4 ++++ tests/PHPStan/Rules/Arrays/data/bug-8549.php | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php index 4216b264b12..e829627c528 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php @@ -252,6 +252,10 @@ public function testBug8549(): void '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 7e3a0d7388b..3e75ef8668f 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php @@ -106,6 +106,10 @@ public function testBug8549(): void 'Bug8549\Options does not accept string.', 92, ], + [ + 'Bug8549\Options does not accept float.', + 109, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-8549.php b/tests/PHPStan/Rules/Arrays/data/bug-8549.php index 02401e2700d..e852babba0a 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-8549.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-8549.php @@ -93,3 +93,21 @@ public function compound(Options $options): void } } + +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'; + } + +} From 609d653e936d7c1114c50a397cac7587e0c754ac Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 1 Nov 2026 23:17:00 -0800 Subject: [PATCH 6/8] Check an appended value against offsetSet() with a null offset For an append (`$object[] = $value`), OffsetAccessValueAssignmentRule checked the value against a mixed offset, while the write itself goes through ObjectType::setOffsetValueType() with a null offset. Since offsetSet()'s parameters are now resolved against the written offset, a value type depending on the offset, such as `@param ($offset is null ? int : string) $value`, resolved to `int|string` for the check and `$object[] = 'str'` was no longer reported. The rule now passes the append's null offset on to setOffsetValueType(), and falls back to a mixed offset only when the append itself is rejected, as for any other rejected offset. Co-Authored-By: Claude Opus 5.5 (high effort) Claude-Code-Session-Id: 781e7324-e13c-4956-a86a-9047e742d5b9 --- .../OffsetAccessValueAssignmentRule.php | 22 +++++------ .../OffsetAccessValueAssignmentRuleTest.php | 15 +++++++ .../offset-access-value-assignment-append.php | 39 +++++++++++++++++++ 3 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment-append.php diff --git a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php index 54f191f029e..7ba8fdc8901 100644 --- a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php @@ -64,20 +64,18 @@ public function processNode(Node $node, Scope $scope): array $assignedValueType = $scope->getType($node); } - // The offset type lets method-level template 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. + // 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 ($dimType !== null) { - 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; - } + 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; diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php index 3e75ef8668f..6ea11443aa6 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php @@ -113,4 +113,19 @@ public function testBug8549(): void ]); } + #[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/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; +} From 00c3fedc91a45afa6dd09a9ef79af18ce6714e1f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 1 Nov 2026 23:23:08 -0800 Subject: [PATCH 7/8] Cover ArrayAccess variant selection in the type-family differential The native ObjectType now selects the offsetGet()/offsetSet() variant from the offset and value types, but type-family.php only probed the offset operations on plain ObjectType(ArrayAccess) and ArrayObject, which never reach the template resolution. A fixture adds an ArrayAccess class whose offsetGet() and offsetSet() declare `@template TOffset of key-of`, and one whose offsetSet() value type is conditional on the offset. getOffsetValueType(), hasOffsetValueType() and setOffsetValueType(), appends included, are observed on them with known, unknown and union keys, alongside a generic ArrayObject for the path without anything to resolve. Against a build of the native ObjectType without the variant selection, 96 of these observations differ from the PHP twin. Co-Authored-By: Claude Opus 5.5 (high effort) Claude-Code-Session-Id: 781e7324-e13c-4956-a86a-9047e742d5b9 --- .../type-family-array-access-fixture.php | 95 +++++++++++++++++++ turbo-ext/tests/type-family.php | 45 +++++++++ 2 files changed, 140 insertions(+) create mode 100644 turbo-ext/tests/type-family-array-access-fixture.php 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; } From 0b3ccbf80af7865bbeaa3166192dcf1f6a86dbb3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 1 Nov 2026 23:26:17 -0800 Subject: [PATCH 8/8] Regenerate the native ObjectType declarations The private ObjectType::selectArrayAccessVariant() added for the ArrayAccess fast path is part of the declarations that turbo-ext/bin/generate-declarations.php derives from the PHP class, so turbo-ext/src/generated/ObjectType.h was stale and turbo-ext/bin/side-by-side.php reported it. Co-Authored-By: Claude Opus 5.5 (high effort) Claude-Code-Session-Id: 781e7324-e13c-4956-a86a-9047e742d5b9 --- turbo-ext/src/generated/ObjectType.h | 181 ++++++++++++++------------- 1 file changed, 94 insertions(+), 87 deletions(-) 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