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()) {
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()->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;
}
}

return $sanitizedTypes;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPUnit\Framework\Attributes\RequiresPhp;
use ThrowsVoidMethod\MyException;
use UnhandledMatchError;
use ValueError;

/**
* @extends RuleTestCase<ThrowsVoidMethodWithExplicitThrowPointRule>
Expand Down Expand Up @@ -99,6 +100,13 @@ public function testRule(bool $missingCheckedExceptionInThrows, array $checkedEx
$this->analyse([__DIR__ . '/data/throws-void-method.php'], $errors);
}

public function testBug13642(): void
{
$this->missingCheckedExceptionInThrows = false;
$this->checkedExceptionClasses = [ValueError::class];
$this->analyse([__DIR__ . '/data/bug-13642.php'], []);
}

#[RequiresPhp('>= 8.0')]
public function testBug6910(): void
{
Expand Down
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/Exceptions/data/bug-13642.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php declare(strict_types = 1);

namespace Bug13642;

class HelloWorld
{
/** @throws void */
public function sayHello(): void
{
array_combine([1, 2], [1, 2]);
}
}
Loading