diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 9f77b0c8db0..f7e1aa61073 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -4,7 +4,6 @@ use Closure; use PhpParser\Node; -use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; @@ -20,7 +19,7 @@ use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\EarlyTerminatingCallHelper; -use PHPStan\Analyser\ExprHandler\Helper\OutputBufferHelper; +use PHPStan\Analyser\ExprHandler\Helper\FuncCallScopeEffectsHelper; use PHPStan\Analyser\ExprHandler\Helper\VoidToNullTypeTransformer; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; @@ -36,8 +35,6 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\ExtensionsCollection; use PHPStan\Node\ClosureReturnStatementsNode; -use PHPStan\Node\Expr\NativeTypeExpr; -use PHPStan\Node\Expr\PossiblyImpureCallExpr; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Callables\SimpleImpurePoint; @@ -48,44 +45,26 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; -use PHPStan\TrinaryLogic; -use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\HasPropertyType; -use PHPStan\Type\Accessory\NonEmptyArrayType; -use PHPStan\Type\ArrayType; -use PHPStan\Type\ClosureType; -use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\DynamicReturnTypeExtensionRegistry; use PHPStan\Type\ErrorType; -use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Generic\TemplateTypeVarianceMap; -use PHPStan\Type\IntegerRangeType; -use PHPStan\Type\IntegerType; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; -use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; use Throwable; use function array_filter; use function array_map; use function array_merge; -use function array_slice; use function array_values; use function count; use function in_array; use function is_string; -use function sprintf; -use function str_starts_with; /** * @implements ExprHandler @@ -105,8 +84,7 @@ public function __construct( private DynamicReturnTypeExtensionRegistry $dynamicReturnTypeExtensionRegistry, #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] private bool $implicitThrows, - #[AutowiredParameter] - private bool $rememberPossiblyImpureFunctionValues, + private FuncCallScopeEffectsHelper $funcCallScopeEffectsHelper, private ExpressionResultFactory $expressionResultFactory, private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, ) @@ -318,19 +296,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } if ($arrayWalkValueTypes !== null && $arrayWalkArrayArg !== null) { - $arrayWalkValueType = $arrayWalkValueTypes[0]; - $arrayWalkValueNativeType = $arrayWalkValueTypes[1]; - $newArrayType = $arrayWalkOriginalArrayType->mapValueType(static fn (Type $type): Type => $arrayWalkValueType); - $newArrayNativeType = $arrayWalkOriginalArrayNativeType->mapValueType(static fn (Type $type): Type => $arrayWalkValueNativeType); - - $scope = $nodeScopeResolver->processVirtualAssign( - $scope, - $storage, - $stmt, - $arrayWalkArrayArg, - new NativeTypeExpr($newArrayType, $newArrayNativeType), - $nodeCallback, - )->getScope(); + $scope = $this->funcCallScopeEffectsHelper->applyArrayWalkResult($nodeScopeResolver, $stmt, $arrayWalkArrayArg, $arrayWalkValueTypes, $arrayWalkOriginalArrayType, $arrayWalkOriginalArrayNativeType, $scope, $storage, $nodeCallback); } if ($normalizedExpr->name instanceof Expr) { @@ -370,263 +336,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); } - if ( - $parametersAcceptor instanceof ClosureType && count($parametersAcceptor->getImpurePoints()) > 0 - && $scope->isInClass() - ) { - $scope = $scope->invalidateExpression(new Variable('this'), true); - } - - if ( - $functionReflection !== null - && $this->rememberPossiblyImpureFunctionValues - && $functionReflection->hasSideEffects()->maybe() - && !$functionReflection->isBuiltin() - ) { - $scope = $scope->assignExpression( - new PossiblyImpureCallExpr($normalizedExpr, $normalizedExpr, sprintf('%s()', $functionReflection->getName())), - $parametersAcceptor->getReturnType(), - new MixedType(), - ); - } - - if ( - $functionReflection !== null - && in_array($functionReflection->getName(), ['json_encode', 'json_decode'], true) - ) { - $scope = $scope->invalidateExpression(new FuncCall(new Name('json_last_error'), [])) - ->invalidateExpression(new FuncCall(new Name\FullyQualified('json_last_error'), [])) - ->invalidateExpression(new FuncCall(new Name('json_last_error_msg'), [])) - ->invalidateExpression(new FuncCall(new Name\FullyQualified('json_last_error_msg'), [])); - } - - if ( - $functionReflection !== null - && $functionReflection->getName() === 'file_put_contents' - && count($normalizedExpr->getArgs()) > 0 - ) { - $scope = $scope->invalidateExpression(new FuncCall(new Name('file_get_contents'), [$normalizedExpr->getArgs()[0]])) - ->invalidateExpression(new FuncCall(new Name\FullyQualified('file_get_contents'), [$normalizedExpr->getArgs()[0]])); - } - - if ( - $functionReflection !== null - && in_array($functionReflection->getName(), ['array_pop', 'array_shift'], true) - && count($normalizedExpr->getArgs()) >= 1 - ) { - $arrayArg = $normalizedExpr->getArgs()[0]->value; - - $arrayArgType = $scope->getType($arrayArg); - $arrayArgNativeType = $scope->getNativeType($arrayArg); - $isArrayPop = $functionReflection->getName() === 'array_pop'; - - $scope = $nodeScopeResolver->processVirtualAssign( - $scope, - $storage, - $stmt, - $arrayArg, - new NativeTypeExpr( - $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(), - $isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(), - ), - $nodeCallback, - )->getScope(); - } - - if ( - $functionReflection !== null - && in_array($functionReflection->getName(), ['array_push', 'array_unshift'], true) - && count($normalizedExpr->getArgs()) >= 2 - ) { - $arrayArg = $normalizedExpr->getArgs()[0]->value; - - $scope = $nodeScopeResolver->processVirtualAssign( - $scope, - $storage, - $stmt, - $arrayArg, - new NativeTypeExpr( - $this->getArrayFunctionAppendingType($functionReflection, $scopeBeforeArgs, $normalizedExpr), - $this->getArrayFunctionAppendingType($functionReflection, $scopeBeforeArgs->doNotTreatPhpDocTypesAsCertain(), $normalizedExpr), - ), - $nodeCallback, - )->getScope(); - } - - if ( - $functionReflection !== null - && in_array($functionReflection->getName(), ['fopen', 'file_get_contents'], true) - ) { - $scope = $scope->assignVariable('http_response_header', new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()), new AccessoryArrayListType()]), new ArrayType(new IntegerType(), new StringType()), TrinaryLogic::createYes()); - } - - if ( - $functionReflection !== null - && $functionReflection->getName() === 'shuffle' - ) { - $arrayArg = $normalizedExpr->getArgs()[0]->value; - - $scope = $nodeScopeResolver->processVirtualAssign( - $scope, - $storage, - $stmt, - $arrayArg, - new NativeTypeExpr($scope->getType($arrayArg)->shuffleArray(), $scope->getNativeType($arrayArg)->shuffleArray()), - $nodeCallback, - )->getScope(); - } - - if ( - $functionReflection !== null - && $functionReflection->getName() === 'array_splice' - && count($normalizedExpr->getArgs()) >= 2 - ) { - $arrayArg = $normalizedExpr->getArgs()[0]->value; - $arrayArgType = $scope->getType($arrayArg); - $arrayArgNativeType = $scope->getNativeType($arrayArg); - - $offsetType = $scopeBeforeArgs->getType($normalizedExpr->getArgs()[1]->value); - - if (isset($normalizedExpr->getArgs()[2])) { - $lengthType = $scopeBeforeArgs->getType($normalizedExpr->getArgs()[2]->value); - } else { - $lengthType = new NullType(); - } - - if (isset($normalizedExpr->getArgs()[3])) { - $replacementArg = $normalizedExpr->getArgs()[3]->value; - $replacementType = $scopeBeforeArgs->getType($replacementArg); - $replacementNativeType = $scopeBeforeArgs->getNativeType($replacementArg); - } else { - $replacementType = new ConstantArrayType([], []); - $replacementNativeType = new ConstantArrayType([], []); - } - - $scope = $nodeScopeResolver->processVirtualAssign( - $scope, - $storage, - $stmt, - $arrayArg, - new NativeTypeExpr( - $arrayArgType->spliceArray($offsetType, $lengthType, $replacementType), - $arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementNativeType), - ), - $nodeCallback, - )->getScope(); - } - - if ( - $functionReflection !== null - && in_array($functionReflection->getName(), ['sort', 'rsort', 'usort'], true) - && count($normalizedExpr->getArgs()) >= 1 - ) { - $arrayArg = $normalizedExpr->getArgs()[0]->value; - - $scope = $nodeScopeResolver->processVirtualAssign( - $scope, - $storage, - $stmt, - $arrayArg, - new NativeTypeExpr($scope->getType($arrayArg)->shuffleArray(), $scope->getNativeType($arrayArg)->shuffleArray()), - $nodeCallback, - )->getScope(); - } - - if ( - $functionReflection !== null - && in_array($functionReflection->getName(), ['natcasesort', 'natsort', 'arsort', 'asort', 'ksort', 'krsort', 'uasort', 'uksort'], true) - && count($normalizedExpr->getArgs()) >= 1 - ) { - $arrayArg = $normalizedExpr->getArgs()[0]->value; - - $scope = $nodeScopeResolver->processVirtualAssign( - $scope, - $storage, - $stmt, - $arrayArg, - new NativeTypeExpr($scope->getType($arrayArg)->makeListMaybe(), $scope->getNativeType($arrayArg)->makeListMaybe()), - $nodeCallback, - )->getScope(); - } - - if ( - $functionReflection !== null - && $functionReflection->getName() === 'extract' - ) { - $extractedArg = $normalizedExpr->getArgs()[0]->value; - $extractedType = $scope->getType($extractedArg); - $constantArrays = $extractedType->getConstantArrays(); - if (count($constantArrays) > 0) { - $properties = []; - $optionalProperties = []; - $refCount = []; - foreach ($constantArrays as $constantArray) { - foreach ($constantArray->getKeyTypes() as $i => $keyType) { - if ($keyType->isString()->no()) { - // integers as variable names not allowed - continue; - } - $key = (string) $keyType->getValue(); - $valueType = $constantArray->getValueTypes()[$i]; - $optional = $constantArray->isOptionalKey($i); - if ($optional) { - $optionalProperties[] = $key; - } - if (isset($properties[$key])) { - $properties[$key] = TypeCombinator::union($properties[$key], $valueType); - $refCount[$key]++; - } else { - $properties[$key] = $valueType; - $refCount[$key] = 1; - } - } - } - foreach ($properties as $name => $type) { - $optional = in_array($name, $optionalProperties, true) || $refCount[$name] < count($constantArrays); - - if (!$optional) { - $scope = $scope->assignVariable($name, $type, $type, TrinaryLogic::createYes()); - } else { - $hasVariable = $scope->hasVariableType($name); - if (!$hasVariable->no()) { - $type = TypeCombinator::union($scope->getVariableType($name), $type); - } - - $scope = $scope->assignVariable($name, $type, $type, $scope->hasVariableType($name)->or(TrinaryLogic::createMaybe())); - } - } - } else { - $scope = $scope->afterExtractCall(); - } - } - - if ( - $functionReflection !== null - && in_array($functionReflection->getName(), ['clearstatcache', 'unlink'], true) - ) { - $scope = $scope->afterClearstatcacheCall(); - } - - if ( - $functionReflection !== null - && str_starts_with($functionReflection->getName(), 'openssl') - ) { - $scope = $scope->afterOpenSslCall($functionReflection->getName()); - } - - $outputBufferDelta = $functionReflection !== null ? OutputBufferHelper::getLevelDelta($functionReflection->getName()) : 0; - if ($outputBufferDelta !== 0) { - $scope = OutputBufferHelper::applyLevelDelta($scope, $outputBufferDelta); - } - - $pureCallable = $parametersAcceptor instanceof CallableParametersAcceptor - && count($parametersAcceptor->getImpurePoints()) === 0; - if ( - ($functionReflection !== null && !$functionReflection->isBuiltin() && !$functionReflection->hasSideEffects()->no()) - || ($functionReflection === null && !$pureCallable) - ) { - $scope = $scope->invalidateVolatileExpressions(); - } + $scope = $this->funcCallScopeEffectsHelper->applyCallScopeEffects($nodeScopeResolver, $stmt, $normalizedExpr, $functionReflection, $parametersAcceptor, $scope, $scopeBeforeArgs, $storage, $nodeCallback); return $this->expressionResultFactory->create( $scope, @@ -700,149 +410,6 @@ private function getFunctionThrowPoint( return null; } - private function getArrayFunctionAppendingType(FunctionReflection $functionReflection, Scope $scope, FuncCall $expr): Type - { - $arrayArg = $expr->getArgs()[0]->value; - $arrayType = $scope->getType($arrayArg); - $callArgs = array_slice($expr->getArgs(), 1); - - /** - * @param Arg[] $callArgs - * @param callable(?Type, Type, bool): void $setOffsetValueType - */ - $setOffsetValueTypes = static function (Scope $scope, array $callArgs, callable $setOffsetValueType, ?bool &$nonConstantArrayWasUnpacked = null): void { - foreach ($callArgs as $callArg) { - $callArgType = $scope->getType($callArg->value); - if ($callArg->unpack) { - $constantArrays = $callArgType->getConstantArrays(); - if (count($constantArrays) === 1) { - $iterableValueTypes = $constantArrays[0]->getValueTypes(); - } else { - $iterableValueTypes = [$callArgType->getIterableValueType()]; - $nonConstantArrayWasUnpacked = true; - } - - $isOptional = !$callArgType->isIterableAtLeastOnce()->yes(); - foreach ($iterableValueTypes as $iterableValueType) { - if ($iterableValueType instanceof UnionType) { - foreach ($iterableValueType->getTypes() as $innerType) { - $setOffsetValueType(null, $innerType, $isOptional); - } - } else { - $setOffsetValueType(null, $iterableValueType, $isOptional); - } - } - continue; - } - $setOffsetValueType(null, $callArgType, false); - } - }; - - $constantArrays = $arrayType->getConstantArrays(); - if (count($constantArrays) > 0) { - $newArrayTypes = []; - $prepend = $functionReflection->getName() === 'array_unshift'; - foreach ($constantArrays as $constantArray) { - $arrayTypeBuilder = $prepend ? ConstantArrayTypeBuilder::createEmpty() : ConstantArrayTypeBuilder::createFromConstantArray($constantArray); - - $setOffsetValueTypes( - $scope, - $callArgs, - static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayTypeBuilder): void { - $arrayTypeBuilder->setOffsetValueType($offsetType, $valueType, $optional); - }, - $nonConstantArrayWasUnpacked, - ); - - if ($prepend) { - $keyTypes = $constantArray->getKeyTypes(); - $valueTypes = $constantArray->getValueTypes(); - foreach ($keyTypes as $k => $keyType) { - $arrayTypeBuilder->setOffsetValueType( - count($keyType->getConstantStrings()) === 1 ? $keyType->getConstantStrings()[0] : null, - $valueTypes[$k], - $constantArray->isOptionalKey($k), - ); - } - - $unsealedTypes = $constantArray->getUnsealedTypes(); - if ($unsealedTypes !== null) { - $arrayTypeBuilder->makeUnsealed($unsealedTypes[0], $unsealedTypes[1]); - } - } - - $constantArray = $arrayTypeBuilder->getArray(); - - if ($constantArray->isConstantArray()->yes() && $nonConstantArrayWasUnpacked) { - $constantArrays = $constantArray->getConstantArrays(); - if ($constantArray->isList()->yes()) { - // A list can't preserve precise indices when an - // unknown number of values is prepended/appended — - // every index would be shifted by an unknown - // amount. Degrade to a `non-empty-list<...>` of - // the value union. - $array = new ArrayType($constantArray->generalize(GeneralizePrecision::lessSpecific())->getIterableKeyType(), $constantArray->getIterableValueType()); - $constantArray = $constantArray->isIterableAtLeastOnce()->yes() - ? new IntersectionType([$array, new NonEmptyArrayType()]) - : $array; - $constantArray = TypeCombinator::intersect($constantArray, new AccessoryArrayListType()); - } elseif (count($constantArrays) === 1) { - // Associative input — string keys keep their - // precise values and the unknown count of - // unpacked items lives in an unsealed `int` slot - // of the result. Drops the auto-indexed - // representatives that the unpacked-arg loop - // inserted (they stand in for "0..N-1 of the - // unpack value type" and are now subsumed by the - // unsealed slot). - $builder = ConstantArrayTypeBuilder::createEmpty(); - $intValues = []; - foreach ($constantArrays[0]->getKeyTypes() as $i => $keyType) { - $valueType = $constantArrays[0]->getValueTypes()[$i]; - if ($keyType->isString()->yes()) { - $builder->setOffsetValueType($keyType, $valueType, $constantArrays[0]->isOptionalKey($i)); - continue; - } - $intValues[] = $valueType; - } - - $unsealedKey = new IntegerType(); - $unsealedValue = count($intValues) > 0 ? TypeCombinator::union(...$intValues) : new MixedType(); - if ($constantArrays[0]->isUnsealed()->yes()) { - $existing = $constantArrays[0]->getUnsealedTypes(); - if ($existing !== null) { - $unsealedKey = TypeCombinator::union($unsealedKey, $existing[0]); - $unsealedValue = TypeCombinator::union($unsealedValue, $existing[1]); - } - } - $builder->makeUnsealed($unsealedKey, $unsealedValue); - $constantArray = $builder->getArray(); - } - } - - $newArrayTypes[] = $constantArray; - } - - return TypeCombinator::union(...$newArrayTypes); - } - - $setOffsetValueTypes( - $scope, - $callArgs, - static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayType): void { - $isIterableAtLeastOnce = $arrayType->isIterableAtLeastOnce()->yes() || !$optional; - $arrayType = $arrayType->setOffsetValueType($offsetType, $valueType); - if ($isIterableAtLeastOnce) { - return; - } - - $arrayType = TypeCombinator::union($arrayType, new ConstantArrayType([], [])); - }, - ); - - return $arrayType; - } - public function resolveType(MutatingScope $scope, Expr $expr): Type { if ( diff --git a/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php b/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php new file mode 100644 index 00000000000..acd77f873b6 --- /dev/null +++ b/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php @@ -0,0 +1,500 @@ +mapValueType(static fn (Type $type): Type => $arrayWalkValueType); + $newArrayNativeType = $arrayWalkOriginalArrayNativeType->mapValueType(static fn (Type $type): Type => $arrayWalkValueNativeType); + + $scope = $nodeScopeResolver->processVirtualAssign( + $scope, + $storage, + $stmt, + $arrayWalkArrayArg, + new NativeTypeExpr($newArrayType, $newArrayNativeType), + $nodeCallback, + )->getScope(); + + return $scope; + } + + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, FuncCall $normalizedExpr, ?FunctionReflection $functionReflection, ?ParametersAcceptor $parametersAcceptor, MutatingScope $scope, MutatingScope $scopeBeforeArgs, ExpressionResultStorage $storage, callable $nodeCallback): MutatingScope + { + if ( + $parametersAcceptor instanceof ClosureType && count($parametersAcceptor->getImpurePoints()) > 0 + && $scope->isInClass() + ) { + $scope = $scope->invalidateExpression(new Variable('this'), true); + } + + if ( + $functionReflection !== null + && $parametersAcceptor !== null + && $this->rememberPossiblyImpureFunctionValues + && $functionReflection->hasSideEffects()->maybe() + && !$functionReflection->isBuiltin() + ) { + $scope = $scope->assignExpression( + new PossiblyImpureCallExpr($normalizedExpr, $normalizedExpr, sprintf('%s()', $functionReflection->getName())), + $parametersAcceptor->getReturnType(), + new MixedType(), + ); + } + + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['json_encode', 'json_decode'], true) + ) { + $scope = $scope->invalidateExpression(new FuncCall(new Name('json_last_error'), [])) + ->invalidateExpression(new FuncCall(new Name\FullyQualified('json_last_error'), [])) + ->invalidateExpression(new FuncCall(new Name('json_last_error_msg'), [])) + ->invalidateExpression(new FuncCall(new Name\FullyQualified('json_last_error_msg'), [])); + } + + if ( + $functionReflection !== null + && $functionReflection->getName() === 'file_put_contents' + && count($normalizedExpr->getArgs()) > 0 + ) { + $scope = $scope->invalidateExpression(new FuncCall(new Name('file_get_contents'), [$normalizedExpr->getArgs()[0]])) + ->invalidateExpression(new FuncCall(new Name\FullyQualified('file_get_contents'), [$normalizedExpr->getArgs()[0]])); + } + + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['array_pop', 'array_shift'], true) + && count($normalizedExpr->getArgs()) >= 1 + ) { + $arrayArg = $normalizedExpr->getArgs()[0]->value; + + $arrayArgType = $scope->getType($arrayArg); + $arrayArgNativeType = $scope->getNativeType($arrayArg); + $isArrayPop = $functionReflection->getName() === 'array_pop'; + + $scope = $nodeScopeResolver->processVirtualAssign( + $scope, + $storage, + $stmt, + $arrayArg, + new NativeTypeExpr( + $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(), + $isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(), + ), + $nodeCallback, + )->getScope(); + } + + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['array_push', 'array_unshift'], true) + && count($normalizedExpr->getArgs()) >= 2 + ) { + $arrayArg = $normalizedExpr->getArgs()[0]->value; + + $scope = $nodeScopeResolver->processVirtualAssign( + $scope, + $storage, + $stmt, + $arrayArg, + new NativeTypeExpr( + $this->getArrayFunctionAppendingType($functionReflection, $scopeBeforeArgs, $normalizedExpr), + $this->getArrayFunctionAppendingType($functionReflection, $scopeBeforeArgs->doNotTreatPhpDocTypesAsCertain(), $normalizedExpr), + ), + $nodeCallback, + )->getScope(); + } + + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['fopen', 'file_get_contents'], true) + ) { + $scope = $scope->assignVariable('http_response_header', new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()), new AccessoryArrayListType()]), new ArrayType(new IntegerType(), new StringType()), TrinaryLogic::createYes()); + } + + if ( + $functionReflection !== null + && $functionReflection->getName() === 'shuffle' + ) { + $arrayArg = $normalizedExpr->getArgs()[0]->value; + + $scope = $nodeScopeResolver->processVirtualAssign( + $scope, + $storage, + $stmt, + $arrayArg, + new NativeTypeExpr($scope->getType($arrayArg)->shuffleArray(), $scope->getNativeType($arrayArg)->shuffleArray()), + $nodeCallback, + )->getScope(); + } + + if ( + $functionReflection !== null + && $functionReflection->getName() === 'array_splice' + && count($normalizedExpr->getArgs()) >= 2 + ) { + $arrayArg = $normalizedExpr->getArgs()[0]->value; + $arrayArgType = $scope->getType($arrayArg); + $arrayArgNativeType = $scope->getNativeType($arrayArg); + + $offsetType = $scopeBeforeArgs->getType($normalizedExpr->getArgs()[1]->value); + + if (isset($normalizedExpr->getArgs()[2])) { + $lengthType = $scopeBeforeArgs->getType($normalizedExpr->getArgs()[2]->value); + } else { + $lengthType = new NullType(); + } + + if (isset($normalizedExpr->getArgs()[3])) { + $replacementArg = $normalizedExpr->getArgs()[3]->value; + $replacementType = $scopeBeforeArgs->getType($replacementArg); + $replacementNativeType = $scopeBeforeArgs->getNativeType($replacementArg); + } else { + $replacementType = new ConstantArrayType([], []); + $replacementNativeType = new ConstantArrayType([], []); + } + + $scope = $nodeScopeResolver->processVirtualAssign( + $scope, + $storage, + $stmt, + $arrayArg, + new NativeTypeExpr( + $arrayArgType->spliceArray($offsetType, $lengthType, $replacementType), + $arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementNativeType), + ), + $nodeCallback, + )->getScope(); + } + + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['sort', 'rsort', 'usort'], true) + && count($normalizedExpr->getArgs()) >= 1 + ) { + $arrayArg = $normalizedExpr->getArgs()[0]->value; + + $scope = $nodeScopeResolver->processVirtualAssign( + $scope, + $storage, + $stmt, + $arrayArg, + new NativeTypeExpr($scope->getType($arrayArg)->shuffleArray(), $scope->getNativeType($arrayArg)->shuffleArray()), + $nodeCallback, + )->getScope(); + } + + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['natcasesort', 'natsort', 'arsort', 'asort', 'ksort', 'krsort', 'uasort', 'uksort'], true) + && count($normalizedExpr->getArgs()) >= 1 + ) { + $arrayArg = $normalizedExpr->getArgs()[0]->value; + + $scope = $nodeScopeResolver->processVirtualAssign( + $scope, + $storage, + $stmt, + $arrayArg, + new NativeTypeExpr($scope->getType($arrayArg)->makeListMaybe(), $scope->getNativeType($arrayArg)->makeListMaybe()), + $nodeCallback, + )->getScope(); + } + + if ( + $functionReflection !== null + && $functionReflection->getName() === 'extract' + ) { + $extractedArg = $normalizedExpr->getArgs()[0]->value; + $extractedType = $scope->getType($extractedArg); + $constantArrays = $extractedType->getConstantArrays(); + if (count($constantArrays) > 0) { + $properties = []; + $optionalProperties = []; + $refCount = []; + foreach ($constantArrays as $constantArray) { + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + if ($keyType->isString()->no()) { + // integers as variable names not allowed + continue; + } + $key = (string) $keyType->getValue(); + $valueType = $constantArray->getValueTypes()[$i]; + $optional = $constantArray->isOptionalKey($i); + if ($optional) { + $optionalProperties[] = $key; + } + if (isset($properties[$key])) { + $properties[$key] = TypeCombinator::union($properties[$key], $valueType); + $refCount[$key]++; + } else { + $properties[$key] = $valueType; + $refCount[$key] = 1; + } + } + } + foreach ($properties as $name => $type) { + $optional = in_array($name, $optionalProperties, true) || $refCount[$name] < count($constantArrays); + + if (!$optional) { + $scope = $scope->assignVariable($name, $type, $type, TrinaryLogic::createYes()); + } else { + $hasVariable = $scope->hasVariableType($name); + if (!$hasVariable->no()) { + $type = TypeCombinator::union($scope->getVariableType($name), $type); + } + + $scope = $scope->assignVariable($name, $type, $type, $scope->hasVariableType($name)->or(TrinaryLogic::createMaybe())); + } + } + } else { + $scope = $scope->afterExtractCall(); + } + } + + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['clearstatcache', 'unlink'], true) + ) { + $scope = $scope->afterClearstatcacheCall(); + } + + if ( + $functionReflection !== null + && str_starts_with($functionReflection->getName(), 'openssl') + ) { + $scope = $scope->afterOpenSslCall($functionReflection->getName()); + } + + $outputBufferDelta = $functionReflection !== null ? OutputBufferHelper::getLevelDelta($functionReflection->getName()) : 0; + if ($outputBufferDelta !== 0) { + $scope = OutputBufferHelper::applyLevelDelta($scope, $outputBufferDelta); + } + + $pureCallable = $parametersAcceptor instanceof CallableParametersAcceptor + && count($parametersAcceptor->getImpurePoints()) === 0; + if ( + ($functionReflection !== null && !$functionReflection->isBuiltin() && !$functionReflection->hasSideEffects()->no()) + || ($functionReflection === null && !$pureCallable) + ) { + $scope = $scope->invalidateVolatileExpressions(); + } + + return $scope; + } + + private function getArrayFunctionAppendingType(FunctionReflection $functionReflection, Scope $scope, FuncCall $expr): Type + { + $arrayArg = $expr->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + $callArgs = array_slice($expr->getArgs(), 1); + + /** + * @param Arg[] $callArgs + * @param callable(?Type, Type, bool): void $setOffsetValueType + */ + $setOffsetValueTypes = static function (Scope $scope, array $callArgs, callable $setOffsetValueType, ?bool &$nonConstantArrayWasUnpacked = null): void { + foreach ($callArgs as $callArg) { + $callArgType = $scope->getType($callArg->value); + if ($callArg->unpack) { + $constantArrays = $callArgType->getConstantArrays(); + if (count($constantArrays) === 1) { + $iterableValueTypes = $constantArrays[0]->getValueTypes(); + } else { + $iterableValueTypes = [$callArgType->getIterableValueType()]; + $nonConstantArrayWasUnpacked = true; + } + + $isOptional = !$callArgType->isIterableAtLeastOnce()->yes(); + foreach ($iterableValueTypes as $iterableValueType) { + if ($iterableValueType instanceof UnionType) { + foreach ($iterableValueType->getTypes() as $innerType) { + $setOffsetValueType(null, $innerType, $isOptional); + } + } else { + $setOffsetValueType(null, $iterableValueType, $isOptional); + } + } + continue; + } + $setOffsetValueType(null, $callArgType, false); + } + }; + + $constantArrays = $arrayType->getConstantArrays(); + if (count($constantArrays) > 0) { + $newArrayTypes = []; + $prepend = $functionReflection->getName() === 'array_unshift'; + foreach ($constantArrays as $constantArray) { + $arrayTypeBuilder = $prepend ? ConstantArrayTypeBuilder::createEmpty() : ConstantArrayTypeBuilder::createFromConstantArray($constantArray); + + $setOffsetValueTypes( + $scope, + $callArgs, + static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayTypeBuilder): void { + $arrayTypeBuilder->setOffsetValueType($offsetType, $valueType, $optional); + }, + $nonConstantArrayWasUnpacked, + ); + + if ($prepend) { + $keyTypes = $constantArray->getKeyTypes(); + $valueTypes = $constantArray->getValueTypes(); + foreach ($keyTypes as $k => $keyType) { + $arrayTypeBuilder->setOffsetValueType( + count($keyType->getConstantStrings()) === 1 ? $keyType->getConstantStrings()[0] : null, + $valueTypes[$k], + $constantArray->isOptionalKey($k), + ); + } + + $unsealedTypes = $constantArray->getUnsealedTypes(); + if ($unsealedTypes !== null) { + $arrayTypeBuilder->makeUnsealed($unsealedTypes[0], $unsealedTypes[1]); + } + } + + $constantArray = $arrayTypeBuilder->getArray(); + + if ($constantArray->isConstantArray()->yes() && $nonConstantArrayWasUnpacked) { + $constantArrays = $constantArray->getConstantArrays(); + if ($constantArray->isList()->yes()) { + // A list can't preserve precise indices when an + // unknown number of values is prepended/appended — + // every index would be shifted by an unknown + // amount. Degrade to a `non-empty-list<...>` of + // the value union. + $array = new ArrayType($constantArray->generalize(GeneralizePrecision::lessSpecific())->getIterableKeyType(), $constantArray->getIterableValueType()); + $constantArray = $constantArray->isIterableAtLeastOnce()->yes() + ? new IntersectionType([$array, new NonEmptyArrayType()]) + : $array; + $constantArray = TypeCombinator::intersect($constantArray, new AccessoryArrayListType()); + } elseif (count($constantArrays) === 1) { + // Associative input — string keys keep their + // precise values and the unknown count of + // unpacked items lives in an unsealed `int` slot + // of the result. Drops the auto-indexed + // representatives that the unpacked-arg loop + // inserted (they stand in for "0..N-1 of the + // unpack value type" and are now subsumed by the + // unsealed slot). + $builder = ConstantArrayTypeBuilder::createEmpty(); + $intValues = []; + foreach ($constantArrays[0]->getKeyTypes() as $i => $keyType) { + $valueType = $constantArrays[0]->getValueTypes()[$i]; + if ($keyType->isString()->yes()) { + $builder->setOffsetValueType($keyType, $valueType, $constantArrays[0]->isOptionalKey($i)); + continue; + } + $intValues[] = $valueType; + } + + $unsealedKey = new IntegerType(); + $unsealedValue = count($intValues) > 0 ? TypeCombinator::union(...$intValues) : new MixedType(); + if ($constantArrays[0]->isUnsealed()->yes()) { + $existing = $constantArrays[0]->getUnsealedTypes(); + if ($existing !== null) { + $unsealedKey = TypeCombinator::union($unsealedKey, $existing[0]); + $unsealedValue = TypeCombinator::union($unsealedValue, $existing[1]); + } + } + $builder->makeUnsealed($unsealedKey, $unsealedValue); + $constantArray = $builder->getArray(); + } + } + + $newArrayTypes[] = $constantArray; + } + + return TypeCombinator::union(...$newArrayTypes); + } + + $setOffsetValueTypes( + $scope, + $callArgs, + static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayType): void { + $isIterableAtLeastOnce = $arrayType->isIterableAtLeastOnce()->yes() || !$optional; + $arrayType = $arrayType->setOffsetValueType($offsetType, $valueType); + if ($isIterableAtLeastOnce) { + return; + } + + $arrayType = TypeCombinator::union($arrayType, new ConstantArrayType([], [])); + }, + ); + + return $arrayType; + } + +}