Skip to content

Commit 8f09e01

Browse files
committed
Merge branch 'add-phpstan-extension'
2 parents 4129e84 + 9f59b4c commit 8f09e01

24 files changed

+332
-56
lines changed

phpstan.extension.neon

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,26 @@ services:
55
- phpstan.rules.rule
66

77
-
8-
class: Salient\PHPStan\Type\Core\ArrWhereNotEmptyMethodReturnTypeExtension
8+
class: Salient\PHPStan\Type\Core\ArrExtendReturnTypeExtension
99
tags:
1010
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
1111

1212
-
13-
class: Salient\PHPStan\Type\Core\ArrWhereNotNullMethodReturnTypeExtension
13+
class: Salient\PHPStan\Type\Core\ArrWhereNotEmptyReturnTypeExtension
1414
tags:
1515
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
1616

1717
-
18-
class: Salient\PHPStan\Type\Core\GetCoalesceMethodReturnTypeExtension
18+
class: Salient\PHPStan\Type\Core\ArrWhereNotNullReturnTypeExtension
1919
tags:
2020
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
2121

2222
-
23-
class: Salient\PHPStan\Type\Core\StrCoalesceMethodReturnTypeExtension
23+
class: Salient\PHPStan\Type\Core\GetCoalesceReturnTypeExtension
24+
tags:
25+
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
26+
27+
-
28+
class: Salient\PHPStan\Type\Core\StrCoalesceReturnTypeExtension
2429
tags:
2530
- phpstan.broker.dynamicStaticMethodReturnTypeExtension

phpstan.neon.dist

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,24 @@ parameters:
2424
identifier: salient.needless.coalesce
2525
paths:
2626
- tests/fixtures/Toolkit/PHPStan/Rules/Core/GetCoalesceRuleFailures.php
27-
- tests/fixtures/Toolkit/PHPStan/Type/Core/GetCoalesceMethodReturnTypeExtensionAssertions.php
27+
- tests/fixtures/Toolkit/PHPStan/Type/Core/GetCoalesceReturnTypeExtensionAssertions.php
28+
-
29+
identifier: arguments.count
30+
paths:
31+
- tests/fixtures/Toolkit/PHPStan/Type/Core/ArrExtendReturnTypeExtensionAssertions.php
32+
- tests/fixtures/Toolkit/PHPStan/Type/Core/ArrWhereNotEmptyReturnTypeExtensionAssertions.php
33+
- tests/fixtures/Toolkit/PHPStan/Type/Core/ArrWhereNotNullReturnTypeExtensionAssertions.php
2834
-
2935
identifier: argument.templateType
3036
paths:
3137
- tests/fixtures/Toolkit/PHPStan/Rules/Core/GetCoalesceRuleFailures.php
32-
- tests/fixtures/Toolkit/PHPStan/Type/Core/GetCoalesceMethodReturnTypeExtensionAssertions.php
38+
- tests/fixtures/Toolkit/PHPStan/Type/Core/ArrExtendReturnTypeExtensionAssertions.php
39+
- tests/fixtures/Toolkit/PHPStan/Type/Core/ArrWhereNotEmptyReturnTypeExtensionAssertions.php
40+
- tests/fixtures/Toolkit/PHPStan/Type/Core/ArrWhereNotNullReturnTypeExtensionAssertions.php
41+
- tests/fixtures/Toolkit/PHPStan/Type/Core/GetCoalesceReturnTypeExtensionAssertions.php
42+
-
43+
identifier: argument.type
44+
paths:
45+
- tests/fixtures/Toolkit/PHPStan/Type/Core/ArrExtendReturnTypeExtensionAssertions.php
46+
- tests/fixtures/Toolkit/PHPStan/Type/Core/ArrWhereNotEmptyReturnTypeExtensionAssertions.php
47+
- tests/fixtures/Toolkit/PHPStan/Type/Core/ArrWhereNotNullReturnTypeExtensionAssertions.php
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Salient\PHPStan\Type\Core;
4+
5+
use PhpParser\Node\Expr\StaticCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\MethodReflection;
8+
use PHPStan\Type\Accessory\AccessoryArrayListType;
9+
use PHPStan\Type\Accessory\NonEmptyArrayType;
10+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
11+
use PHPStan\Type\ArrayType;
12+
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
13+
use PHPStan\Type\GeneralizePrecision;
14+
use PHPStan\Type\IntegerType;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeCombinator;
17+
use Salient\Utility\Arr;
18+
19+
class ArrExtendReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
20+
{
21+
/**
22+
* @codeCoverageIgnore
23+
*/
24+
public function getClass(): string
25+
{
26+
return Arr::class;
27+
}
28+
29+
public function isStaticMethodSupported(
30+
MethodReflection $methodReflection
31+
): bool {
32+
return $methodReflection->getName() === 'extend';
33+
}
34+
35+
public function getTypeFromStaticMethodCall(
36+
MethodReflection $methodReflection,
37+
StaticCall $methodCall,
38+
Scope $scope
39+
): ?Type {
40+
$args = $methodCall->getArgs();
41+
42+
if ($args === []) {
43+
return null;
44+
}
45+
46+
// Get the `$array` argument and its type
47+
$arrayArg = array_shift($args);
48+
$arrayType = $scope->getType($arrayArg->value);
49+
50+
if ($arrayType->isArray()->no()) {
51+
return null;
52+
}
53+
54+
// If there are no more arguments, do nothing
55+
if ($args === []) {
56+
return $arrayType;
57+
}
58+
59+
// Otherwise, check if `$array` and every subsequent argument is a
60+
// constant
61+
$constant = $arrayType->isConstantArray()->yes()
62+
&& count($arrayType->getConstantArrays()) === 1;
63+
64+
$argTypes = [];
65+
foreach ($args as $arg) {
66+
$type = $scope->getType($arg->value);
67+
$argTypes[] = [$type, $arg->unpack];
68+
69+
if (!$constant) {
70+
continue;
71+
}
72+
73+
// Unpack variadic arguments
74+
if ($arg->unpack) {
75+
$constant = $type->isConstantArray()->yes()
76+
&& count($type->getConstantArrays()) === 1;
77+
continue;
78+
}
79+
80+
$constant = $type->isConstantValue()->yes();
81+
}
82+
83+
// If so, build a new constant array type by adding elements to `$array`
84+
if ($constant) {
85+
[$array] = $arrayType->getConstantArrays();
86+
$builder = ConstantArrayTypeBuilder::createFromConstantArray($array);
87+
$arrayValueType = $array->getIterableValueType();
88+
89+
// Generalize the array if `$array` has any optional elements with
90+
// numeric keys
91+
$generalize = false;
92+
if ($optionalKeys = $array->getOptionalKeys()) {
93+
$keyTypes = $array->getKeyTypes();
94+
$int = new IntegerType();
95+
foreach ($optionalKeys as $key) {
96+
if (!$int->isSuperTypeOf($keyTypes[$key])->no()) {
97+
$generalize = true;
98+
break;
99+
}
100+
}
101+
}
102+
103+
foreach ($argTypes as [$argType, $unpack]) {
104+
if ($unpack) {
105+
[$array] = $argType->getConstantArrays();
106+
foreach ($array->getValueTypes() as $i => $valueType) {
107+
// Ignore values already present in `$array`
108+
if ($arrayValueType->isSuperTypeOf($valueType)->yes()) {
109+
continue;
110+
}
111+
// Generalize the array if any variadic arguments after
112+
// `$array` have optional elements
113+
$optional = false;
114+
if ($array->isOptionalKey($i)) {
115+
$optional = true;
116+
$generalize = true;
117+
}
118+
$builder->setOffsetValueType(null, $valueType, $optional);
119+
}
120+
continue;
121+
}
122+
123+
if ($arrayValueType->isSuperTypeOf($argType)->yes()) {
124+
continue;
125+
}
126+
$builder->setOffsetValueType(null, $argType);
127+
}
128+
129+
$built = $builder->getArray();
130+
return $generalize
131+
? $built->generalize(GeneralizePrecision::lessSpecific())
132+
: $built;
133+
}
134+
135+
// Otherwise, add `int` to the array's key type, and the types of
136+
// subsequent arguments to its value type
137+
$keyType = $arrayType->getIterableKeyType();
138+
$valueType = $arrayType->getIterableValueType();
139+
$nonEmpty = $arrayType->isIterableAtLeastOnce()->yes();
140+
$keyType = TypeCombinator::union($keyType, new IntegerType());
141+
foreach ($argTypes as [$argType, $unpack]) {
142+
if ($unpack) {
143+
$nonEmpty = $nonEmpty || $argType->isIterableAtLeastOnce()->yes();
144+
$argType = $argType->getIterableValueType();
145+
} else {
146+
$nonEmpty = true;
147+
}
148+
$valueType = TypeCombinator::union($valueType, $argType);
149+
}
150+
$type = new ArrayType($keyType, $valueType);
151+
if ($nonEmpty) {
152+
$type = TypeCombinator::intersect($type, new NonEmptyArrayType());
153+
}
154+
if ($arrayType->isList()->yes()) {
155+
$type = AccessoryArrayListType::intersectWith($type);
156+
}
157+
return $type;
158+
}
159+
}

src/Toolkit/PHPStan/Type/Core/ArrWhereNotEmptyMethodReturnTypeExtension.php renamed to src/Toolkit/PHPStan/Type/Core/ArrWhereNotEmptyReturnTypeExtension.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use PHPStan\Type\Constant\ConstantStringType;
1111
use PHPStan\Type\ArrayType;
1212
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
13-
use PHPStan\Type\ErrorType;
1413
use PHPStan\Type\NullType;
1514
use PHPStan\Type\ObjectType;
1615
use PHPStan\Type\Type;
@@ -19,7 +18,7 @@
1918
use Salient\Utility\Arr;
2019
use Stringable;
2120

22-
class ArrWhereNotEmptyMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
21+
class ArrWhereNotEmptyReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
2322
{
2423
/**
2524
* @codeCoverageIgnore
@@ -43,13 +42,13 @@ public function getTypeFromStaticMethodCall(
4342
$args = $methodCall->getArgs();
4443

4544
if ($args === []) {
46-
return new ErrorType();
45+
return null;
4746
}
4847

4948
$type = $scope->getType($args[0]->value);
5049

5150
if ($type->isIterable()->no()) {
52-
return new ErrorType();
51+
return null;
5352
}
5453

5554
$empty = new UnionType([

src/Toolkit/PHPStan/Type/Core/ArrWhereNotNullMethodReturnTypeExtension.php renamed to src/Toolkit/PHPStan/Type/Core/ArrWhereNotNullReturnTypeExtension.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@
88
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
99
use PHPStan\Type\ArrayType;
1010
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
11-
use PHPStan\Type\ErrorType;
1211
use PHPStan\Type\NullType;
1312
use PHPStan\Type\Type;
1413
use PHPStan\Type\TypeCombinator;
1514
use Salient\Utility\Arr;
1615

17-
class ArrWhereNotNullMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
16+
class ArrWhereNotNullReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
1817
{
1918
/**
2019
* @codeCoverageIgnore
@@ -38,13 +37,13 @@ public function getTypeFromStaticMethodCall(
3837
$args = $methodCall->getArgs();
3938

4039
if ($args === []) {
41-
return new ErrorType();
40+
return null;
4241
}
4342

4443
$type = $scope->getType($args[0]->value);
4544

4645
if ($type->isIterable()->no()) {
47-
return new ErrorType();
46+
return null;
4847
}
4948

5049
$null = new NullType();

src/Toolkit/PHPStan/Type/Core/GetCoalesceMethodReturnTypeExtension.php renamed to src/Toolkit/PHPStan/Type/Core/GetCoalesceReturnTypeExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use Salient\Utility\Arr;
1515
use Salient\Utility\Get;
1616

17-
class GetCoalesceMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
17+
class GetCoalesceReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
1818
{
1919
/**
2020
* @codeCoverageIgnore

src/Toolkit/PHPStan/Type/Core/StrCoalesceMethodReturnTypeExtension.php renamed to src/Toolkit/PHPStan/Type/Core/StrCoalesceReturnTypeExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use Salient\Utility\Arr;
1616
use Salient\Utility\Str;
1717

18-
class StrCoalesceMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
18+
class StrCoalesceReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
1919
{
2020
/**
2121
* @codeCoverageIgnore

src/Toolkit/Utility/Arr.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,10 @@ public static function push(array $array, ...$values): array
189189
*
190190
* @template TKey of array-key
191191
* @template TValue
192-
* @template T
193192
*
194193
* @param array<TKey,TValue> $array
195-
* @param T ...$values
196-
* @return array<TKey|int,TValue|T>
194+
* @param TValue ...$values
195+
* @return array<TKey|int,TValue>
197196
*/
198197
public static function extend(array $array, ...$values): array
199198
{
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Salient\Tests\PHPStan\Type\Core;
4+
5+
use Salient\Utility\Arr;
6+
use ReflectionClass;
7+
use ReflectionFunction;
8+
use ReflectionFunctionAbstract;
9+
use ReflectionMethod;
10+
use ReflectionObject;
11+
use Reflector;
12+
use Stringable;
13+
14+
use function PHPStan\Testing\assertType;
15+
16+
$a = [Reflector::class, ReflectionFunction::class, ReflectionObject::class];
17+
$b = [ReflectionFunction::class, ReflectionMethod::class];
18+
$c = [ReflectionClass::class, ReflectionObject::class];
19+
/** @var class-string<Reflector>[] */
20+
$h = [];
21+
/** @var class-string<ReflectionFunctionAbstract>[] */
22+
$i = [];
23+
/** @var class-string<ReflectionClass<object>>[] */
24+
$j = [];
25+
/** @var array{foo?:string,bar:int,baz?:string} */
26+
$d = [];
27+
/** @var array{foo?:string,bar:int,0?:string} */
28+
$e = [];
29+
/** @var array{Stringable|string,int|float|null,2:bool,true} */
30+
$f = [];
31+
/** @var array{Stringable|string,int|float,2?:bool,true} */
32+
$g = [];
33+
/** @var non-empty-array<mixed> */
34+
$k = [];
35+
/** @var mixed[] */
36+
$l = [];
37+
/** @var list<mixed> */
38+
$m = [];
39+
40+
assertType('array', Arr::extend());
41+
assertType('array', Arr::extend('foo'));
42+
assertType("array{'a', 'a', 'd', 'd', 'b', 'b', 'c', 'c'}", Arr::extend(['a', 'a', 'd', 'd'], 'a', 'a', 'a', 'b', 'b', 'c', 'c'));
43+
assertType("array{foo: 'a', bar: 'd', 0: 'b', 1: 'c'}", Arr::extend(['foo' => 'a', 'bar' => 'd'], 'a', 'b', 'c'));
44+
assertType("array{'Reflector', 'ReflectionFunction', 'ReflectionObject', 'ReflectionMethod', 'ReflectionClass'}", Arr::extend($a, ...$b, ...$c));
45+
assertType("array{'ReflectionFunction', 'ReflectionMethod'}", Arr::extend($b));
46+
assertType("array{'ReflectionFunction', 'ReflectionMethod', 'ReflectionClass', 'ReflectionObject'}", Arr::extend($b, ...$c));
47+
assertType(sprintf('array<class-string<%s>>', Reflector::class), Arr::extend($h, ...$i, ...$j));
48+
assertType(sprintf('array<class-string<%s>>', ReflectionFunctionAbstract::class), Arr::extend($i));
49+
assertType(sprintf('array<class-string<%s<object>>|class-string<%s>>', ReflectionClass::class, ReflectionFunctionAbstract::class), Arr::extend($i, ...$j));
50+
assertType('array{foo?: string, bar: int, baz?: string, 0: string|Stringable, 1: float|int|null, 2: bool, 3: true}', Arr::extend($d, ...$f));
51+
assertType('non-empty-array<int|string, bool|float|int|string|Stringable>', Arr::extend($d, ...$g));
52+
assertType('non-empty-array<int|string, bool|float|int|string|Stringable|null>', Arr::extend($e, ...$f));
53+
assertType('non-empty-array<int|string, bool|float|int|string|Stringable>', Arr::extend($e, ...$g));
54+
assertType('non-empty-array', Arr::extend($k));
55+
assertType('non-empty-array', Arr::extend($k, 71));
56+
assertType('array', Arr::extend($l));
57+
assertType('non-empty-array', Arr::extend($l, 71));
58+
assertType('non-empty-array', Arr::extend($l, ...$k));
59+
assertType('array', Arr::extend($l, ...$l));
60+
assertType('non-empty-array', Arr::extend($l, $l));
61+
assertType('array<int, mixed>', Arr::extend($m));
62+
assertType('non-empty-array<int, mixed>', Arr::extend($m, 71));

0 commit comments

Comments
 (0)