diff --git a/src/Analyser/ExprHandler/Helper/ContextualClosureParameterResolver.php b/src/Analyser/ExprHandler/Helper/ContextualClosureParameterResolver.php index 504f95b166e..ea8bb455b22 100644 --- a/src/Analyser/ExprHandler/Helper/ContextualClosureParameterResolver.php +++ b/src/Analyser/ExprHandler/Helper/ContextualClosureParameterResolver.php @@ -13,6 +13,7 @@ use PHPStan\Parser\ArrowFunctionArgVisitor; use PHPStan\Parser\ClosureArgVisitor; use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; +use PHPStan\Reflection\ArrayMapCallbackParameterTypes; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\PassedByReference; @@ -90,17 +91,38 @@ public function resolve( ); } - $parameters = []; - $nativeParameters = []; + $types = []; + $nativeTypes = []; + $arrayTypes = []; + $nativeArrayTypes = []; + $hasUnpackedArg = false; foreach ($intrinsicArgs as $arg) { + $hasUnpackedArg = $hasUnpackedArg || $arg->unpack; $result = $storage !== null ? $storage->findExpressionResult($arg->value) : null; $type = $result !== null ? $result->getType() : $this->nodeScopeResolver->readScopeStateOrSyntheticType($arg->value, $scope); $nativeType = $result !== null ? $result->getNativeType() : $this->nodeScopeResolver->readScopeStateOrSyntheticType($arg->value, $scope->doNotTreatPhpDocTypesAsCertain()); if ($arrayMapArgs !== null) { + $arrayTypes[] = $type; + $nativeArrayTypes[] = $nativeType; $type = $type->getIterableValueType(); $nativeType = $nativeType->getIterableValueType(); } + $types[] = $type; + $nativeTypes[] = $nativeType; + } + + // an unpacked arg is one parameter here, so its arrays cannot be told apart + if ($arrayMapArgs !== null && !$hasUnpackedArg) { + $types = ArrayMapCallbackParameterTypes::addPaddingNull($types, $arrayTypes); + $nativeTypes = ArrayMapCallbackParameterTypes::addPaddingNull($nativeTypes, $nativeArrayTypes); + } + + $parameters = []; + foreach ($types as $type) { $parameters[] = new DummyParameter('item', $type, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + } + $nativeParameters = []; + foreach ($nativeTypes as $nativeType) { $nativeParameters[] = new DummyParameter('item', $nativeType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); } diff --git a/src/Reflection/ArrayMapCallbackParameterTypes.php b/src/Reflection/ArrayMapCallbackParameterTypes.php new file mode 100644 index 00000000000..4af2907f3a1 --- /dev/null +++ b/src/Reflection/ArrayMapCallbackParameterTypes.php @@ -0,0 +1,70 @@ + $valueTypes + * @param list $arrayTypes the array each value type comes from + * @return list + */ + public static function addPaddingNull(array $valueTypes, array $arrayTypes): array + { + if (count($arrayTypes) < 2) { + return $valueTypes; + } + + $maxSizes = []; + foreach ($arrayTypes as $i => $arrayType) { + $sizes = []; + foreach ($arrayType->getArraySize()->getFiniteTypes() as $sizeType) { + foreach ($sizeType->getConstantScalarValues() as $size) { + if (!is_int($size)) { + continue; + } + + $sizes[] = $size; + } + } + + if ($sizes === []) { + // general arrays can have the same size, but the type system cannot say so + return $valueTypes; + } + + $maxSizes[$i] = max($sizes); + } + + $longestSize = max($maxSizes); + foreach ($valueTypes as $i => $valueType) { + if ($maxSizes[$i] === $longestSize) { + continue; + } + + $valueTypes[$i] = TypeCombinator::addNull($valueType); + } + + return $valueTypes; + } + +} diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 22c137ede82..b4411c63882 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -206,8 +206,10 @@ public static function applyIntrinsicArgOverrides( ) { $arrayMapArgs = $args[0]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); if ($arrayMapArgs !== null) { - $callbackParameters = []; - $nativeCallbackParameters = []; + $callbackValueTypes = []; + $callbackArrayTypes = []; + $nativeCallbackValueTypes = []; + $nativeCallbackArrayTypes = []; foreach ($arrayMapArgs as $arg) { $argType = ($typeGetter)($arg->value); $nativeArgType = ($nativeTypeGetter)($arg->value); @@ -217,7 +219,8 @@ public static function applyIntrinsicArgOverrides( foreach ($constantArrays as $constantArray) { $valueTypes = $constantArray->getValueTypes(); foreach ($valueTypes as $valueType) { - $callbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($valueType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $callbackValueTypes[] = ($iterableValueTypeGetter)($valueType); + $callbackArrayTypes[] = $valueType; } } } @@ -226,16 +229,28 @@ public static function applyIntrinsicArgOverrides( foreach ($nativeConstantArrays as $constantArray) { $valueTypes = $constantArray->getValueTypes(); foreach ($valueTypes as $valueType) { - $nativeCallbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($valueType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $nativeCallbackValueTypes[] = ($iterableValueTypeGetter)($valueType); + $nativeCallbackArrayTypes[] = $valueType; } } } } else { - $callbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); - $nativeCallbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $callbackValueTypes[] = ($iterableValueTypeGetter)($argType); + $callbackArrayTypes[] = $argType; + $nativeCallbackValueTypes[] = ($iterableValueTypeGetter)($nativeArgType); + $nativeCallbackArrayTypes[] = $nativeArgType; } } + $callbackParameters = []; + foreach (ArrayMapCallbackParameterTypes::addPaddingNull($callbackValueTypes, $callbackArrayTypes) as $valueType) { + $callbackParameters[] = new DummyParameter('item', $valueType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + } + $nativeCallbackParameters = []; + foreach (ArrayMapCallbackParameterTypes::addPaddingNull($nativeCallbackValueTypes, $nativeCallbackArrayTypes) as $valueType) { + $nativeCallbackParameters[] = new DummyParameter('item', $valueType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + } + $acceptor = $parametersAcceptors[0]; $parameters = $acceptor->getParameters(); if (isset($parameters[0])) { diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index a9766d7ceba..4fcdd30ff37 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -8,6 +8,7 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Parser\ArrayMapArgVisitor; +use PHPStan\Reflection\ArrayMapCallbackParameterTypes; use PHPStan\Reflection\FunctionReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; @@ -25,6 +26,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function array_filter; use function array_map; use function array_reduce; use function array_slice; @@ -53,11 +55,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $callableIsNull = $callableType->isNull()->yes(); if ($callableType->isCallable()->yes()) { + $arrayArgs = array_slice($args, 1); + $arrayTypes = array_map(static fn (Node\Arg $arg) => $scope->getType($arg->value), $arrayArgs); + $callbackValueTypes = array_map(static fn (Type $arrayType) => $arrayType->getIterableValueType(), $arrayTypes); + if (count(array_filter($arrayArgs, static fn (Node\Arg $arg) => $arg->unpack)) === 0) { + $callbackValueTypes = ArrayMapCallbackParameterTypes::addPaddingNull($callbackValueTypes, $arrayTypes); + } $valueType = $scope->getType(new FuncCall( $callback, array_map( - static fn (Node\Arg $arg) => new Node\Arg(new TypeExpr($scope->getType($arg->value)->getIterableValueType())), - array_slice($args, 1), + static fn (Type $valueType) => new Node\Arg(new TypeExpr($valueType)), + $callbackValueTypes, ), )); } elseif ($callableIsNull) { diff --git a/src/Type/Php/ArrayMapParameterClosureTypeExtension.php b/src/Type/Php/ArrayMapParameterClosureTypeExtension.php index 6ae1c8198a0..0b492d114ae 100644 --- a/src/Type/Php/ArrayMapParameterClosureTypeExtension.php +++ b/src/Type/Php/ArrayMapParameterClosureTypeExtension.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\ArrayMapCallbackParameterTypes; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; @@ -31,7 +32,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return null; } - $callbackParameters = []; + $valueTypes = []; + $arrayTypes = []; $argCount = count($args); for ($i = 1; $i < $argCount; $i++) { $arg = $args[$i]; @@ -43,14 +45,21 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } foreach ($constantArrays as $constantArray) { foreach ($constantArray->getValueTypes() as $valueType) { - $callbackParameters[] = $this->createParameter($scope->getIterableValueType($valueType)); + $valueTypes[] = $scope->getIterableValueType($valueType); + $arrayTypes[] = $valueType; } } } else { - $callbackParameters[] = $this->createParameter($scope->getIterableValueType($arrayType)); + $valueTypes[] = $scope->getIterableValueType($arrayType); + $arrayTypes[] = $arrayType; } } + $callbackParameters = []; + foreach (ArrayMapCallbackParameterTypes::addPaddingNull($valueTypes, $arrayTypes) as $valueType) { + $callbackParameters[] = $this->createParameter($valueType); + } + return new ClosureType($callbackParameters, new MixedType()); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-13217.php b/tests/PHPStan/Analyser/nsrt/bug-13217.php new file mode 100644 index 00000000000..e7ea5277de1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13217.php @@ -0,0 +1,110 @@ + assertType('3|null', $b), [1, 2], [3]); + + assertType('non-empty-list<1|3>', array_map(fn ($a, $b) => $b ?? 1, [1, 2], [3])); + } + + public function shorterFirstArray(): void + { + array_map(function ($a, $b) { + assertType('1|null', $a); + assertType('3|4', $b); + assertNativeType('1|null', $a); + assertNativeType('3|4', $b); + }, [1], [3, 4]); + } + + public function sameSize(): void + { + array_map(function ($a, $b) { + assertType('1|2', $a); + assertType('3|4', $b); + }, [1, 2], [3, 4]); + } + + /** + * @param array{1, 2}|array{1} $maybe + */ + public function sizeRange(array $maybe): void + { + array_map(function ($a, $b) { + assertType('1|2|null', $a); + assertType('3|4|5', $b); + assertNativeType('mixed', $a); + assertNativeType('3|4|5', $b); + }, $maybe, [3, 4, 5]); + + array_map(function ($a, $b) { + assertType('1|2', $a); + assertType('3|null', $b); + }, $maybe, [3]); + + array_map(function ($a, $b) { + assertType('1|2', $a); + assertType('1|2', $b); + }, $maybe, $maybe); + } + + public function singleArray(): void + { + array_map(function ($a) { + assertType('1|2', $a); + }, [1, 2]); + } + + /** + * @param list $ints + * @param list $strings + */ + public function generalArrays(array $ints, array $strings): void + { + array_map(function ($a, $b) { + assertType('int', $a); + assertType('string', $b); + }, $ints, $strings); + + array_map(function ($a) { + assertType('int', $a); + }, $ints); + + array_map(function ($a, $b) { + assertType('int', $a); + assertType('3', $b); + }, $ints, [3]); + } + + public function returnType(): void + { + assertType('non-empty-list<3|null>', array_map('Bug13217\\pick', [1, 2], [3])); + } + +} + +/** + * @template T + * @param mixed $a + * @param T $b + * @return T + */ +function pick($a, $b) +{ + return $b; +} diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index c2eb6a030a5..05ba7627ce9 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -706,6 +706,11 @@ public function testInTrait(): void ]); } + public function testBug13217(): void + { + $this->analyse([__DIR__ . '/data/bug-13217.php'], []); + } + public function testBug12780(): void { $this->analyse([__DIR__ . '/data/bug-12780.php'], []); diff --git a/tests/PHPStan/Rules/Variables/data/bug-13217.php b/tests/PHPStan/Rules/Variables/data/bug-13217.php new file mode 100644 index 00000000000..56e9ff422b4 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-13217.php @@ -0,0 +1,3 @@ + $bValue ?? 1, [1, 2], [3]); diff --git a/turbo-ext/src/ContextualClosureParameterResolver.cpp b/turbo-ext/src/ContextualClosureParameterResolver.cpp index c5abcd0eed3..3d22cd29fba 100644 --- a/turbo-ext/src/ContextualClosureParameterResolver.cpp +++ b/turbo-ext/src/ContextualClosureParameterResolver.cpp @@ -48,6 +48,17 @@ void isCallableYesBody(zval *captures, uint32_t argc, zval *argv, zval *return_v ZVAL_BOOL(return_value, isCallable == PT_TRI_YES); } +/* ArrayMapCallbackParameterTypes::addPaddingNull($valueTypes, $arrayTypes) */ +pt_method_site pt_ccpr_add_padding_null_site; +/* $arg->unpack */ +pt_property_site pt_ccpr_arg_unpack_site; + +zv::Val addPaddingNull(zval *valueTypes, zval *arrayTypes) +{ + zv::Args argv{valueTypes, arrayTypes}; + return pt_call_static_cached(pt_ccpr_add_padding_null_site, PT_CLASS_ARRAY_MAP_CALLBACK_PARAMETER_TYPES, PT_LC("addpaddingnull"), 2, argv); +} + } // namespace namespace phpstanturbo { @@ -142,14 +153,22 @@ class ContextualClosureParameterResolver return true; } uint32_t count = zend_hash_num_elements(Z_ARRVAL_P(argsHold.raw())); - zv::Arr parameterList = count > 0 ? zv::Arr::create(count) : zv::Arr::empty(); - zv::Arr nativeParameterList = count > 0 ? zv::Arr::create(count) : zv::Arr::empty(); + zv::Arr typeList = count > 0 ? zv::Arr::create(count) : zv::Arr::empty(); + zv::Arr nativeTypeList = count > 0 ? zv::Arr::create(count) : zv::Arr::empty(); + zv::Arr arrayTypeList = zv::Arr::empty(); + zv::Arr nativeArrayTypeList = zv::Arr::empty(); zval *nodeScopeResolver = OBJ_PROP_NUM(self, slots::nodeScopeResolver); zend_object *no = pt_passed_by_reference_create_no(); if (UNEXPECTED(no == NULL)) return false; zval noZv; ZVAL_OBJ(&noZv, no); + bool hasUnpackedArg = false; for (zv::ArrayEntry entry : zv::ArrRef(argsHold.raw())) { + if (!hasUnpackedArg) { + zval *unpack = ptclosure::prop(pt_ccpr_arg_unpack_site, entry.value().deref().raw(), PT_LC("unpack")); + if (UNEXPECTED(unpack == NULL)) return false; + hasUnpackedArg = zend_is_true(unpack); + } zval *value = ptclosure::prop(ptclosure::argValueSite, entry.value().deref().raw(), PT_LC("value")); if (UNEXPECTED(value == NULL)) return false; zv::Val valueHold = zv::Val::copyOf(zv::Ref(value)); @@ -174,15 +193,40 @@ class ContextualClosureParameterResolver zend_throw_error(NULL, "Call to a member function getIterableValueType() on %s", zend_zval_value_name(type.ref().isObject() ? nativeType.raw() : type.raw())); return false; } + arrayTypeList.push(type.ref()); + nativeArrayTypeList.push(nativeType.ref()); type = pt_type_op(Z_OBJ_P(type.raw()), PT_OP_GET_ITERABLE_VALUE_TYPE, 0, NULL); if (UNEXPECTED(type.isUndef())) return false; nativeType = pt_type_op(Z_OBJ_P(nativeType.raw()), PT_OP_GET_ITERABLE_VALUE_TYPE, 0, NULL); if (UNEXPECTED(nativeType.isUndef())) return false; } - zv::Val parameter = pt_dummy_parameter_new(pt_ccpr_item, type.raw(), false, &noZv, false, NULL); + typeList.push(std::move(type)); + nativeTypeList.push(std::move(nativeType)); + } + + zv::Val types = zv::Val(std::move(typeList)); + zv::Val nativeTypes = zv::Val(std::move(nativeTypeList)); + // an unpacked arg is one parameter here, so its arrays cannot be told apart + if (arrayMapArgs != NULL && !hasUnpackedArg) { + types = addPaddingNull(types.raw(), arrayTypeList.raw()); + if (UNEXPECTED(types.isUndef())) return false; + nativeTypes = addPaddingNull(nativeTypes.raw(), nativeArrayTypeList.raw()); + if (UNEXPECTED(nativeTypes.isUndef())) return false; + } + if (UNEXPECTED(Z_TYPE_P(types.raw()) != IS_ARRAY || Z_TYPE_P(nativeTypes.raw()) != IS_ARRAY)) { + zend_throw_error(NULL, "ArrayMapCallbackParameterTypes::addPaddingNull() did not return an array"); + return false; + } + + zv::Arr parameterList = count > 0 ? zv::Arr::create(count) : zv::Arr::empty(); + for (zv::ArrayEntry entry : zv::ArrRef(types.raw())) { + zv::Val parameter = pt_dummy_parameter_new(pt_ccpr_item, entry.value().deref().raw(), false, &noZv, false, NULL); if (UNEXPECTED(parameter.isUndef())) return false; parameterList.push(std::move(parameter)); - zv::Val nativeParameter = pt_dummy_parameter_new(pt_ccpr_item, nativeType.raw(), false, &noZv, false, NULL); + } + zv::Arr nativeParameterList = count > 0 ? zv::Arr::create(count) : zv::Arr::empty(); + for (zv::ArrayEntry entry : zv::ArrRef(nativeTypes.raw())) { + zv::Val nativeParameter = pt_dummy_parameter_new(pt_ccpr_item, entry.value().deref().raw(), false, &noZv, false, NULL); if (UNEXPECTED(nativeParameter.isUndef())) return false; nativeParameterList.push(std::move(nativeParameter)); } diff --git a/turbo-ext/src/ParametersAcceptorSelector.cpp b/turbo-ext/src/ParametersAcceptorSelector.cpp index c1869f628bd..3f2363e0721 100644 --- a/turbo-ext/src/ParametersAcceptorSelector.cpp +++ b/turbo-ext/src/ParametersAcceptorSelector.cpp @@ -340,6 +340,15 @@ zv::Val genericResolve(zval *types, zval *acceptor) return pt_call_static_cached(pt_pas_generic_resolve_site, PT_CLASS_GENERIC_PARAMETERS_ACCEPTOR_RESOLVER, PT_LC("resolve"), 2, argv); } +/* ArrayMapCallbackParameterTypes::addPaddingNull($valueTypes, $arrayTypes) */ +pt_method_site pt_pas_add_padding_null_site; + +zv::Val addPaddingNull(zval *valueTypes, zval *arrayTypes) +{ + zv::Args argv{valueTypes, arrayTypes}; + return pt_call_static_cached(pt_pas_add_padding_null_site, PT_CLASS_ARRAY_MAP_CALLBACK_PARAMETER_TYPES, PT_LC("addpaddingnull"), 2, argv); +} + /* }}} */ } // namespace @@ -1978,14 +1987,15 @@ class ParametersAcceptorSelector return !parameters.isUndef(); } - /* the array_map() callback parameters of one arg type flavour; false = - * pending exception */ - static bool arrayMapParameters(zv::Arr &callbackParameters, zval *argType, bool unpack, const Getters &getters) + /* the array_map() callback value types of one arg type flavour, with the + * array each comes from; false = pending exception */ + static bool arrayMapValueTypes(zv::Arr &valueTypes, zv::Arr &arrayTypes, zval *argType, bool unpack, const Getters &getters) { if (!unpack) { - zv::Val parameter = dummyParameter(pt_pas_item, getters.iterableValueType(argType)); - if (UNEXPECTED(parameter.isUndef())) return false; - callbackParameters.push(std::move(parameter)); + zv::Val valueType = getters.iterableValueType(argType); + if (UNEXPECTED(valueType.isUndef())) return false; + valueTypes.push(std::move(valueType)); + arrayTypes.push(zv::Ref(argType)); return true; } if (UNEXPECTED(!requireObject(argType, "getConstantArrays"))) return false; @@ -1994,22 +2004,41 @@ class ParametersAcceptorSelector for (zv::ArrayEntry entry : zv::ArrRef(constantArrays.raw())) { zval *constantArray = entry.value().deref().raw(); if (UNEXPECTED(!requireObject(constantArray, "getValueTypes"))) return false; - zv::Val valueTypes = pt_type_op(Z_OBJ_P(constantArray), PT_OP_GET_VALUE_TYPES, 0, NULL); - if (UNEXPECTED(valueTypes.isUndef() || !requireArray(valueTypes.raw(), "foreach() argument"))) return false; - for (zv::ArrayEntry valueEntry : zv::ArrRef(valueTypes.raw())) { - zv::Val parameter = dummyParameter(pt_pas_item, getters.iterableValueType(valueEntry.value().deref().raw())); - if (UNEXPECTED(parameter.isUndef())) return false; - callbackParameters.push(std::move(parameter)); + zv::Val constantValueTypes = pt_type_op(Z_OBJ_P(constantArray), PT_OP_GET_VALUE_TYPES, 0, NULL); + if (UNEXPECTED(constantValueTypes.isUndef() || !requireArray(constantValueTypes.raw(), "foreach() argument"))) return false; + for (zv::ArrayEntry valueEntry : zv::ArrRef(constantValueTypes.raw())) { + zval *arrayType = valueEntry.value().deref().raw(); + zv::Val valueType = getters.iterableValueType(arrayType); + if (UNEXPECTED(valueType.isUndef())) return false; + valueTypes.push(std::move(valueType)); + arrayTypes.push(zv::Ref(arrayType)); } } return true; } + /* the DummyParameters over ArrayMapCallbackParameterTypes::addPaddingNull(); + * UNDEF = pending exception */ + static zv::Val arrayMapParameters(zv::Arr &valueTypes, zv::Arr &arrayTypes) + { + zv::Val paddedValueTypes = addPaddingNull(valueTypes.raw(), arrayTypes.raw()); + if (UNEXPECTED(paddedValueTypes.isUndef() || !requireArray(paddedValueTypes.raw(), "foreach() argument"))) return zv::Val(); + zv::Arr callbackParameters = zv::Arr::empty(); + for (zv::ArrayEntry entry : zv::ArrRef(paddedValueTypes.raw())) { + zv::Val parameter = dummyParameter(pt_pas_item, zv::Val::copyOf(entry.value().deref())); + if (UNEXPECTED(parameter.isUndef())) return zv::Val(); + callbackParameters.push(std::move(parameter)); + } + return zv::Val(std::move(callbackParameters)); + } + /* the ArrayMapArgVisitor override; false = pending exception */ static bool applyArrayMap(zv::Val ¶metersAcceptors, zval *arrayMapArgs, const Getters &getters) { - zv::Arr callbackParameters = zv::Arr::empty(); - zv::Arr nativeCallbackParameters = zv::Arr::empty(); + zv::Arr callbackValueTypes = zv::Arr::empty(); + zv::Arr callbackArrayTypes = zv::Arr::empty(); + zv::Arr nativeCallbackValueTypes = zv::Arr::empty(); + zv::Arr nativeCallbackArrayTypes = zv::Arr::empty(); if (UNEXPECTED(!requireArray(arrayMapArgs, "foreach() argument"))) return false; for (zv::ArrayEntry entry : zv::ArrRef(arrayMapArgs)) { zval *arg = entry.value().deref().raw(); @@ -2023,22 +2052,23 @@ class ParametersAcceptorSelector zv::Val unpackHold; zval *unpack = readProperty(pt_pas_arg_unpack_site, arg, PT_LC("unpack"), unpackHold); if (UNEXPECTED(unpack == NULL)) return false; - if (zend_is_true(unpack)) { - if (UNEXPECTED(!arrayMapParameters(callbackParameters, argType.raw(), true, getters))) return false; - if (UNEXPECTED(!arrayMapParameters(nativeCallbackParameters, nativeArgType.raw(), true, getters))) return false; - } else { - if (UNEXPECTED(!arrayMapParameters(callbackParameters, argType.raw(), false, getters))) return false; - if (UNEXPECTED(!arrayMapParameters(nativeCallbackParameters, nativeArgType.raw(), false, getters))) return false; - } + bool isUnpack = zend_is_true(unpack); + if (UNEXPECTED(!arrayMapValueTypes(callbackValueTypes, callbackArrayTypes, argType.raw(), isUnpack, getters))) return false; + if (UNEXPECTED(!arrayMapValueTypes(nativeCallbackValueTypes, nativeCallbackArrayTypes, nativeArgType.raw(), isUnpack, getters))) return false; } + zv::Val callbackParameters = arrayMapParameters(callbackValueTypes, callbackArrayTypes); + if (UNEXPECTED(callbackParameters.isUndef())) return false; + zv::Val nativeCallbackParameters = arrayMapParameters(nativeCallbackValueTypes, nativeCallbackArrayTypes); + if (UNEXPECTED(nativeCallbackParameters.isUndef())) return false; + zv::Val acceptor, parameters; if (UNEXPECTED(!firstAcceptor(parametersAcceptors, acceptor, parameters))) return false; zval *firstParameter = issetIndex(Z_ARRVAL_P(parameters.raw()), 0); if (firstParameter == NULL) return true; - zv::Val callable = orNull(callableType(zv::Val(std::move(callbackParameters)), pt_type_new_mixed_type())); + zv::Val callable = orNull(callableType(std::move(callbackParameters), pt_type_new_mixed_type())); if (UNEXPECTED(callable.isUndef())) return false; - zv::Val nativeCallable = orNull(callableType(zv::Val(std::move(nativeCallbackParameters)), pt_type_new_mixed_type())); + zv::Val nativeCallable = orNull(callableType(std::move(nativeCallbackParameters), pt_type_new_mixed_type())); if (UNEXPECTED(nativeCallable.isUndef())) return false; zv::Val replaced = replacedParameterAcceptors(acceptor.raw(), parameters, 0, overrideParameterType(firstParameter, callable.raw(), nativeCallable.raw()), true); if (UNEXPECTED(replaced.isUndef())) return false; diff --git a/turbo-ext/src/support.cpp b/turbo-ext/src/support.cpp index e34c9b87288..8dc2e0bdad1 100644 --- a/turbo-ext/src/support.cpp +++ b/turbo-ext/src/support.cpp @@ -415,6 +415,7 @@ static const pt_class_template pt_class_templates[PT_CLASS_COUNT] = { /* PT_CLASS_NATIVE_PHP_TYPE_NODE_HELPER */ {"nativePhpTypeNodeHelper", "PHPStan\\Type\\NativePhpTypeNodeHelper"}, /* PT_CLASS_FULLY_QUALIFIED_NAME_NODE */ {"fullyQualifiedNameNode", "PHPStan\\PhpDocParser\\Ast\\Type\\FullyQualifiedNameNode"}, /* PT_CLASS_TYPE_NODE_DESCRIBER */ {"typeNodeDescriber", "PHPStan\\Type\\TypeNodeDescriber"}, + /* PT_CLASS_ARRAY_MAP_CALLBACK_PARAMETER_TYPES */ {"arrayMapCallbackParameterTypes", "PHPStan\\Reflection\\ArrayMapCallbackParameterTypes"}, }; zend_class_entry *pt_class(int idx) diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 6c0e26f9c3a..ed173b1f57a 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -519,6 +519,8 @@ enum { /* the class names of Type::toPhpDocNode() and how describe() prints them */ PT_CLASS_FULLY_QUALIFIED_NAME_NODE, PT_CLASS_TYPE_NODE_DESCRIBER, + /* the null array_map() passes for the shorter arrays */ + PT_CLASS_ARRAY_MAP_CALLBACK_PARAMETER_TYPES, PT_CLASS_COUNT };