Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 14 additions & 117 deletions src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,25 @@
namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Variable;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\ConstantScalarType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\ErrorType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use function array_key_exists;
use function count;
use function is_int;
use function is_string;

#[AutowiredService]
final class ArrayCombineFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function __construct(private PhpVersion $phpVersion)
public function __construct(
private ArrayCombineHelper $arrayCombineHelper,
private PhpVersion $phpVersion,
)
{
}

Expand All @@ -47,119 +39,24 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
$firstArg = $functionCall->getArgs()[0]->value;
$secondArg = $functionCall->getArgs()[1]->value;

$keysParamType = $scope->getType($firstArg);
$valuesParamType = $scope->getType($secondArg);

$constantKeysArrays = $keysParamType->getConstantArrays();
$constantValuesArrays = $valuesParamType->getConstantArrays();
if (
$constantKeysArrays !== []
&& $constantValuesArrays !== []
&& count($constantKeysArrays) === count($constantValuesArrays)
) {
$results = [];
foreach ($constantKeysArrays as $k => $constantKeysArray) {
$constantValueArrays = $constantValuesArrays[$k];

$keyTypes = $constantKeysArray->getValueTypes();
$valueTypes = $constantValueArrays->getValueTypes();

if (count($keyTypes) !== count($valueTypes)) {
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
return new NeverType();
}
return new ConstantBooleanType(false);
}

$keyTypes = $this->sanitizeConstantArrayKeyTypes($keyTypes);
if ($keyTypes === null) {
continue;
}

$builder = ConstantArrayTypeBuilder::createEmpty();
foreach ($keyTypes as $i => $keyType) {
if (!array_key_exists($i, $valueTypes)) {
$results = [];
break 2;
}
$valueType = $valueTypes[$i];
$builder->setOffsetValueType($keyType, $valueType);
}

$results[] = $builder->getArray();
}

if ($results !== []) {
return TypeCombinator::union(...$results);
}
[$returnType, $hasValueError] = $this->arrayCombineHelper->getReturnAndThrowType($firstArg, $secondArg, $scope);
if ($hasValueError->no()) {
return $returnType;
}

if ($keysParamType->isArray()->yes()) {
$itemType = $keysParamType->getIterableValueType();

if ($itemType->isInteger()->no()) {
if ($itemType->toString() instanceof ErrorType) {
return new NeverType();
}

$keyType = $itemType->toString();
} else {
$keyType = $itemType;
if ($hasValueError->yes()) {
if ($this->phpVersion->throwsValueErrorForInternalFunctions()) {
return new NeverType();
}
} else {
$keyType = new MixedType();
}

$arrayType = new ArrayType(
$keyType,
$valuesParamType->isArray()->yes() ? $valuesParamType->getIterableValueType() : new MixedType(),
);

if ($keysParamType->isIterableAtLeastOnce()->yes() && $valuesParamType->isIterableAtLeastOnce()->yes()) {
$arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
return new ConstantBooleanType(false);
}

if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
return $arrayType;
}

if ($firstArg instanceof Variable && $secondArg instanceof Variable && $firstArg->name === $secondArg->name) {
return $arrayType;
}

return new UnionType([$arrayType, new ConstantBooleanType(false)]);
}

/**
* @param array<int, Type> $types
*
* @return list<ConstantScalarType>|null
*/
private function sanitizeConstantArrayKeyTypes(array $types): ?array
{
$sanitizedTypes = [];

foreach ($types as $type) {
if ($type->isInteger()->no() && ! $type->toString() instanceof ErrorType) {
$type = $type->toString();
}

$scalars = $type->getConstantScalarTypes();
if (count($scalars) === 0) {
return null;
}

foreach ($scalars as $scalar) {
$value = $scalar->getValue();
if (!is_int($value) && !is_string($value)) {
return null;
}

$sanitizedTypes[] = $scalar;
}
if ($this->phpVersion->throwsValueErrorForInternalFunctions()) {
return $returnType;
}

return $sanitizedTypes;
return new UnionType([$returnType, new ConstantBooleanType(false)]);
}

}
43 changes: 43 additions & 0 deletions src/Type/Php/ArrayCombineFunctionThrowTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionThrowTypeExtension;
use PHPStan\Type\Type;
use function count;

#[AutowiredService]
final class ArrayCombineFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension
{

public function __construct(private ArrayCombineHelper $arrayCombineHelper)
{
}

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'array_combine';
}

public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type
{
if (count($funcCall->getArgs()) < 2) {
return $functionReflection->getThrowType();
}

$firstArg = $funcCall->getArgs()[0]->value;
$secondArg = $funcCall->getArgs()[1]->value;

$hasValueError = $this->arrayCombineHelper->getReturnAndThrowType($firstArg, $secondArg, $scope)[1];
if (!$hasValueError->no()) {

Check warning on line 36 in src/Type/Php/ArrayCombineFunctionThrowTypeExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $firstArg = $funcCall->getArgs()[0]->value; $secondArg = $funcCall->getArgs()[1]->value; $hasValueError = $this->arrayCombineHelper->getReturnAndThrowType($firstArg, $secondArg, $scope)[1]; - if (!$hasValueError->no()) { + if ($hasValueError->yes()) { return $functionReflection->getThrowType(); } return null; } }

Check warning on line 36 in src/Type/Php/ArrayCombineFunctionThrowTypeExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $firstArg = $funcCall->getArgs()[0]->value; $secondArg = $funcCall->getArgs()[1]->value; $hasValueError = $this->arrayCombineHelper->getReturnAndThrowType($firstArg, $secondArg, $scope)[1]; - if (!$hasValueError->no()) { + if ($hasValueError->yes()) { return $functionReflection->getThrowType(); } return null; } }

Check warning on line 36 in src/Type/Php/ArrayCombineFunctionThrowTypeExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.2, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $firstArg = $funcCall->getArgs()[0]->value; $secondArg = $funcCall->getArgs()[1]->value; $hasValueError = $this->arrayCombineHelper->getReturnAndThrowType($firstArg, $secondArg, $scope)[1]; - if (!$hasValueError->no()) { + if ($hasValueError->yes()) { return $functionReflection->getThrowType(); } return null; } }
return $functionReflection->getThrowType();
}

return null;
}

}
141 changes: 141 additions & 0 deletions src/Type/Php/ArrayCombineHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Variable;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\ConstantScalarType;
use PHPStan\Type\ErrorType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_key_exists;
use function count;
use function is_int;
use function is_string;

#[AutowiredService]
final class ArrayCombineHelper
{

/**
* @return array{Type, TrinaryLogic} The return type and if a ValueError may occur on PHP8 (and a warning on PHP7).
*/
public function getReturnAndThrowType(Expr $firstArg, Expr $secondArg, Scope $scope): array
{
$keysParamType = $scope->getType($firstArg);
$valuesParamType = $scope->getType($secondArg);

$constantKeysArrays = $keysParamType->getConstantArrays();
$constantValuesArrays = $valuesParamType->getConstantArrays();
if (
$constantKeysArrays !== []
&& $constantValuesArrays !== []
&& count($constantKeysArrays) === count($constantValuesArrays)
) {
$results = [];
foreach ($constantKeysArrays as $k => $constantKeysArray) {
$constantValueArrays = $constantValuesArrays[$k];

$keyTypes = $constantKeysArray->getValueTypes();
$valueTypes = $constantValueArrays->getValueTypes();

if (count($keyTypes) !== count($valueTypes)) {
return [new NeverType(), TrinaryLogic::createYes()];
}

$keyTypes = $this->sanitizeConstantArrayKeyTypes($keyTypes);
if ($keyTypes === null) {
continue;
}

$builder = ConstantArrayTypeBuilder::createEmpty();
foreach ($keyTypes as $i => $keyType) {
if (!array_key_exists($i, $valueTypes)) {
$results = [];
break 2;
}
$valueType = $valueTypes[$i];
$builder->setOffsetValueType($keyType, $valueType);
}

$results[] = $builder->getArray();
}

if ($results !== []) {
return [TypeCombinator::union(...$results), TrinaryLogic::createNo()];
}
}

if ($keysParamType->isArray()->yes()) {
$itemType = $keysParamType->getIterableValueType();

if ($itemType->isInteger()->no()) {
if ($itemType->toString() instanceof ErrorType) {
return [new NeverType(), TrinaryLogic::createNo()];
}

$keyType = $itemType->toString();
} else {
$keyType = $itemType;
}
} else {
$keyType = new MixedType();
}

$arrayType = new ArrayType(
$keyType,
$valuesParamType->isArray()->yes() ? $valuesParamType->getIterableValueType() : new MixedType(),
);

if ($keysParamType->isIterableAtLeastOnce()->yes() && $valuesParamType->isIterableAtLeastOnce()->yes()) {
$arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
}

if ($firstArg instanceof Variable && $secondArg instanceof Variable && $firstArg->name === $secondArg->name) {
return [$arrayType, TrinaryLogic::createNo()];
}

return [$arrayType, TrinaryLogic::createMaybe()];
}

/**
* @param array<int, Type> $types
*
* @return list<ConstantScalarType>|null
*/
private function sanitizeConstantArrayKeyTypes(array $types): ?array
{
$sanitizedTypes = [];

foreach ($types as $type) {
if (!$type->isInteger()->yes() && ! $type->toString() instanceof ErrorType) {
$type = $type->toString();
}

$scalars = $type->getConstantScalarTypes();
if (count($scalars) === 0) {
return null;
}

foreach ($scalars as $scalar) {
$value = $scalar->getValue();
if (!is_int($value) && !is_string($value)) {
return null;
}

$sanitizedTypes[] = $scalar;
}
}

return $sanitizedTypes;
}

}
15 changes: 15 additions & 0 deletions tests/PHPStan/Analyser/nsrt/array-combine-php8.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,18 @@ function bug11819(): void
$types[] = 'foo';
assertType('array{1: false, 2: false, 3: false, 4: \'foo\'}', $types);
}

function withMixed(mixed $mixed1, mixed $mixed2)
{
assertType('array', array_combine($mixed1, $mixed2));
}

/**
* @param 1|true $oneOrBool
*/
function withUnionAsKey(int|bool $oneOrBool)
{
$keys = [$oneOrBool];

assertType("array{1: 'bar'}", array_combine($keys, ['bar']));
}
Loading
Loading