Skip to content

Commit 685a17b

Browse files
Add ArrayCombineFunctionThrowTypeExtension
1 parent 71f7b78 commit 685a17b

File tree

5 files changed

+220
-113
lines changed

5 files changed

+220
-113
lines changed

src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php

Lines changed: 11 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,25 @@
33
namespace PHPStan\Type\Php;
44

55
use PhpParser\Node\Expr\FuncCall;
6-
use PhpParser\Node\Expr\Variable;
76
use PHPStan\Analyser\Scope;
87
use PHPStan\DependencyInjection\AutowiredService;
98
use PHPStan\Php\PhpVersion;
109
use PHPStan\Reflection\FunctionReflection;
11-
use PHPStan\Type\Accessory\NonEmptyArrayType;
12-
use PHPStan\Type\ArrayType;
13-
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1410
use PHPStan\Type\Constant\ConstantBooleanType;
15-
use PHPStan\Type\ConstantScalarType;
1611
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
17-
use PHPStan\Type\ErrorType;
18-
use PHPStan\Type\MixedType;
1912
use PHPStan\Type\NeverType;
2013
use PHPStan\Type\Type;
21-
use PHPStan\Type\TypeCombinator;
2214
use PHPStan\Type\UnionType;
23-
use function array_key_exists;
2415
use function count;
25-
use function is_int;
26-
use function is_string;
2716

2817
#[AutowiredService]
2918
final class ArrayCombineFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
3019
{
3120

32-
public function __construct(private PhpVersion $phpVersion)
21+
public function __construct(
22+
private ArrayCombineHelper $arrayCombineHelper,
23+
private PhpVersion $phpVersion
24+
)
3325
{
3426
}
3527

@@ -47,119 +39,25 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
4739
$firstArg = $functionCall->getArgs()[0]->value;
4840
$secondArg = $functionCall->getArgs()[1]->value;
4941

50-
$keysParamType = $scope->getType($firstArg);
51-
$valuesParamType = $scope->getType($secondArg);
42+
[$arrayType, $hasError] = $this->arrayCombineHelper->getArrayAndThrowType($firstArg, $secondArg, $scope);
5243

53-
$constantKeysArrays = $keysParamType->getConstantArrays();
54-
$constantValuesArrays = $valuesParamType->getConstantArrays();
55-
if (
56-
$constantKeysArrays !== []
57-
&& $constantValuesArrays !== []
58-
&& count($constantKeysArrays) === count($constantValuesArrays)
59-
) {
60-
$results = [];
61-
foreach ($constantKeysArrays as $k => $constantKeysArray) {
62-
$constantValueArrays = $constantValuesArrays[$k];
63-
64-
$keyTypes = $constantKeysArray->getValueTypes();
65-
$valueTypes = $constantValueArrays->getValueTypes();
66-
67-
if (count($keyTypes) !== count($valueTypes)) {
68-
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
69-
return new NeverType();
70-
}
71-
return new ConstantBooleanType(false);
72-
}
73-
74-
$keyTypes = $this->sanitizeConstantArrayKeyTypes($keyTypes);
75-
if ($keyTypes === null) {
76-
continue;
77-
}
78-
79-
$builder = ConstantArrayTypeBuilder::createEmpty();
80-
foreach ($keyTypes as $i => $keyType) {
81-
if (!array_key_exists($i, $valueTypes)) {
82-
$results = [];
83-
break 2;
84-
}
85-
$valueType = $valueTypes[$i];
86-
$builder->setOffsetValueType($keyType, $valueType);
87-
}
88-
89-
$results[] = $builder->getArray();
90-
}
91-
92-
if ($results !== []) {
93-
return TypeCombinator::union(...$results);
94-
}
44+
if ($hasError->no()) {
45+
return $arrayType;
9546
}
9647

97-
if ($keysParamType->isArray()->yes()) {
98-
$itemType = $keysParamType->getIterableValueType();
99-
100-
if ($itemType->isInteger()->no()) {
101-
if ($itemType->toString() instanceof ErrorType) {
102-
return new NeverType();
103-
}
104-
105-
$keyType = $itemType->toString();
106-
} else {
107-
$keyType = $itemType;
48+
if ($hasError->yes()) {
49+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
50+
return new NeverType();
10851
}
109-
} else {
110-
$keyType = new MixedType();
111-
}
112-
113-
$arrayType = new ArrayType(
114-
$keyType,
115-
$valuesParamType->isArray()->yes() ? $valuesParamType->getIterableValueType() : new MixedType(),
116-
);
11752

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

12256
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
12357
return $arrayType;
12458
}
12559

126-
if ($firstArg instanceof Variable && $secondArg instanceof Variable && $firstArg->name === $secondArg->name) {
127-
return $arrayType;
128-
}
129-
13060
return new UnionType([$arrayType, new ConstantBooleanType(false)]);
13161
}
13262

133-
/**
134-
* @param array<int, Type> $types
135-
*
136-
* @return list<ConstantScalarType>|null
137-
*/
138-
private function sanitizeConstantArrayKeyTypes(array $types): ?array
139-
{
140-
$sanitizedTypes = [];
141-
142-
foreach ($types as $type) {
143-
if ($type->isInteger()->no() && ! $type->toString() instanceof ErrorType) {
144-
$type = $type->toString();
145-
}
146-
147-
$scalars = $type->getConstantScalarTypes();
148-
if (count($scalars) === 0) {
149-
return null;
150-
}
151-
152-
foreach ($scalars as $scalar) {
153-
$value = $scalar->getValue();
154-
if (!is_int($value) && !is_string($value)) {
155-
return null;
156-
}
157-
158-
$sanitizedTypes[] = $scalar;
159-
}
160-
}
161-
162-
return $sanitizedTypes;
163-
}
164-
16563
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\DependencyInjection\AutowiredService;
8+
use PHPStan\Php\PhpVersion;
9+
use PHPStan\Reflection\FunctionReflection;
10+
use PHPStan\Type\Constant\ConstantBooleanType;
11+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
12+
use PHPStan\Type\DynamicFunctionThrowTypeExtension;
13+
use PHPStan\Type\NeverType;
14+
use PHPStan\Type\Type;
15+
use PHPStan\Type\UnionType;
16+
use function count;
17+
18+
#[AutowiredService]
19+
final class ArrayCombineFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension
20+
{
21+
22+
public function __construct(private ArrayCombineHelper $arrayCombineHelper)
23+
{
24+
}
25+
26+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
27+
{
28+
return $functionReflection->getName() === 'array_combine';
29+
}
30+
31+
public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type
32+
{
33+
if (count($funcCall->getArgs()) < 2) {
34+
return $functionReflection->getThrowType();
35+
}
36+
37+
$firstArg = $funcCall->getArgs()[0]->value;
38+
$secondArg = $funcCall->getArgs()[1]->value;
39+
40+
$hasError = $this->arrayCombineHelper->getArrayAndThrowType($firstArg, $secondArg, $scope)[1];
41+
if (!$hasError->no()) {
42+
return $functionReflection->getThrowType();
43+
}
44+
45+
return null;
46+
}
47+
48+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr;
6+
use PhpParser\Node\Expr\Variable;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\DependencyInjection\AutowiredService;
9+
use PHPStan\TrinaryLogic;
10+
use PHPStan\Type\Accessory\NonEmptyArrayType;
11+
use PHPStan\Type\ArrayType;
12+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
13+
use PHPStan\Type\ConstantScalarType;
14+
use PHPStan\Type\ErrorType;
15+
use PHPStan\Type\MixedType;
16+
use PHPStan\Type\NeverType;
17+
use PHPStan\Type\Type;
18+
use PHPStan\Type\TypeCombinator;
19+
use function array_key_exists;
20+
use function count;
21+
use function is_int;
22+
use function is_string;
23+
24+
#[AutowiredService]
25+
final class ArrayCombineHelper
26+
{
27+
28+
/**
29+
* @return array{Type, TrinaryLogic} The array result and if an error may occur.
30+
*/
31+
public function getArrayAndThrowType(Expr $firstArg, Expr $secondArg, Scope $scope): array
32+
{
33+
$keysParamType = $scope->getType($firstArg);
34+
$valuesParamType = $scope->getType($secondArg);
35+
36+
$constantKeysArrays = $keysParamType->getConstantArrays();
37+
$constantValuesArrays = $valuesParamType->getConstantArrays();
38+
if (
39+
$constantKeysArrays !== []
40+
&& $constantValuesArrays !== []
41+
&& count($constantKeysArrays) === count($constantValuesArrays)
42+
) {
43+
$results = [];
44+
foreach ($constantKeysArrays as $k => $constantKeysArray) {
45+
$constantValueArrays = $constantValuesArrays[$k];
46+
47+
$keyTypes = $constantKeysArray->getValueTypes();
48+
$valueTypes = $constantValueArrays->getValueTypes();
49+
50+
if (count($keyTypes) !== count($valueTypes)) {
51+
return [new NeverType(), TrinaryLogic::createYes()];
52+
}
53+
54+
$keyTypes = $this->sanitizeConstantArrayKeyTypes($keyTypes);
55+
if ($keyTypes === null) {
56+
continue;
57+
}
58+
59+
$builder = ConstantArrayTypeBuilder::createEmpty();
60+
foreach ($keyTypes as $i => $keyType) {
61+
if (!array_key_exists($i, $valueTypes)) {
62+
$results = [];
63+
break 2;
64+
}
65+
$valueType = $valueTypes[$i];
66+
$builder->setOffsetValueType($keyType, $valueType);
67+
}
68+
69+
$results[] = $builder->getArray();
70+
}
71+
72+
if ($results !== []) {
73+
return [TypeCombinator::union(...$results), TrinaryLogic::createNo()];
74+
}
75+
}
76+
77+
if ($keysParamType->isArray()->yes()) {
78+
$itemType = $keysParamType->getIterableValueType();
79+
80+
if ($itemType->isInteger()->no()) {
81+
if ($itemType->toString() instanceof ErrorType) {
82+
return [new NeverType(), TrinaryLogic::createYes()];
83+
}
84+
85+
$keyType = $itemType->toString();
86+
} else {
87+
$keyType = $itemType;
88+
}
89+
} else {
90+
$keyType = new MixedType();
91+
}
92+
93+
$arrayType = new ArrayType(
94+
$keyType,
95+
$valuesParamType->isArray()->yes() ? $valuesParamType->getIterableValueType() : new MixedType(),
96+
);
97+
98+
if ($keysParamType->isIterableAtLeastOnce()->yes() && $valuesParamType->isIterableAtLeastOnce()->yes()) {
99+
$arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
100+
}
101+
102+
if ($firstArg instanceof Variable && $secondArg instanceof Variable && $firstArg->name === $secondArg->name) {
103+
return [$arrayType, TrinaryLogic::createNo()];
104+
}
105+
106+
return [$arrayType, TrinaryLogic::createMaybe()];
107+
}
108+
109+
/**
110+
* @param array<int, Type> $types
111+
*
112+
* @return list<ConstantScalarType>|null
113+
*/
114+
private function sanitizeConstantArrayKeyTypes(array $types): ?array
115+
{
116+
$sanitizedTypes = [];
117+
118+
foreach ($types as $type) {
119+
if ($type->isInteger()->no() && ! $type->toString() instanceof ErrorType) {
120+
$type = $type->toString();
121+
}
122+
123+
$scalars = $type->getConstantScalarTypes();
124+
if (count($scalars) === 0) {
125+
return null;
126+
}
127+
128+
foreach ($scalars as $scalar) {
129+
$value = $scalar->getValue();
130+
if (!is_int($value) && !is_string($value)) {
131+
return null;
132+
}
133+
134+
$sanitizedTypes[] = $scalar;
135+
}
136+
}
137+
138+
return $sanitizedTypes;
139+
}
140+
141+
}

tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PHPUnit\Framework\Attributes\RequiresPhp;
99
use ThrowsVoidMethod\MyException;
1010
use UnhandledMatchError;
11+
use ValueError;
1112

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

103+
public function testBug13642(): void
104+
{
105+
$this->missingCheckedExceptionInThrows = false;
106+
$this->checkedExceptionClasses = [ValueError::class];
107+
$this->analyse([__DIR__ . '/data/bug-13642.php'], []);
108+
}
109+
102110
#[RequiresPhp('>= 8.0')]
103111
public function testBug6910(): void
104112
{
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug13642;
4+
5+
class HelloWorld
6+
{
7+
/** @throws void */
8+
public function sayHello(): void
9+
{
10+
array_combine([1, 2], [1, 2]);
11+
}
12+
}

0 commit comments

Comments
 (0)