Skip to content

Commit b14c966

Browse files
staabmB.J. Scharp
authored andcommitted
Don't report "no value type specified in iterable type array&callable" (phpstan#5565)
1 parent 263f59b commit b14c966

7 files changed

Lines changed: 128 additions & 71 deletions

File tree

src/Rules/MissingTypehintCheck.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,23 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array
7373
if ($type instanceof AccessoryType) {
7474
return $type;
7575
}
76+
if (
77+
$type instanceof IntersectionType
78+
&& $type->isCallable()->yes()
79+
&& $type->isArray()->yes()
80+
) {
81+
$nonArrayInner = [];
82+
foreach ($type->getTypes() as $innerType) {
83+
if ($innerType->isArray()->yes()) {
84+
continue;
85+
}
86+
$nonArrayInner[] = $innerType;
87+
}
88+
if (count($nonArrayInner) === 1) {
89+
return $traverse($nonArrayInner[0]);
90+
}
91+
return $traverse(new IntersectionType($nonArrayInner));
92+
}
7693
if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {
7794
$iterablesWithMissingValueTypehint = array_merge(
7895
$iterablesWithMissingValueTypehint,

src/Type/IntersectionType.php

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,9 @@ public function getArraySize(): Type
784784

785785
public function getIterableKeyType(): Type
786786
{
787+
if ($this->isCallable()->yes() && $this->isArray()->yes()) {
788+
return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
789+
}
787790
return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
788791
}
789792

@@ -799,7 +802,17 @@ public function getLastIterableKeyType(): Type
799802

800803
public function getIterableValueType(): Type
801804
{
802-
return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
805+
$result = $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
806+
if ($this->isCallable()->yes() && $this->isArray()->yes()) {
807+
return TypeCombinator::intersect(
808+
$result,
809+
new UnionType([
810+
new ObjectWithoutClassType(),
811+
new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]),
812+
]),
813+
);
814+
}
815+
return $result;
803816
}
804817

805818
public function getFirstIterableValueType(): Type
@@ -967,17 +980,15 @@ private function doHasOffsetValueType(Type $offsetType): TrinaryLogic
967980
}
968981
}
969982

970-
$result = $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
971-
972-
if (!$result->yes() && $this->isCallable()->yes() && $this->isArray()->yes()) {
983+
if ($this->isCallable()->yes() && $this->isArray()->yes()) {
973984
$arrayKeyOffsetType = $offsetType->toArrayKey();
974985
$callableArrayOffsetType = new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
975986
if ($callableArrayOffsetType->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
976987
return TrinaryLogic::createYes();
977988
}
978989
}
979990

980-
return $result;
991+
return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
981992
}
982993

983994
public function getOffsetValueType(Type $offsetType): Type
@@ -998,17 +1009,14 @@ private function doGetOffsetValueType(Type $offsetType): Type
9981009

9991010
if ($this->isCallable()->yes() && $this->isArray()->yes()) {
10001011
$arrayKeyOffsetType = $offsetType->toArrayKey();
1001-
$callableArrayOffsetType = new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
1002-
if ($callableArrayOffsetType->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1003-
if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1004-
$narrowedType = new UnionType([new ClassStringType(), new ObjectWithoutClassType()]);
1005-
} elseif ((new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1006-
$narrowedType = new StringType();
1007-
} else {
1008-
$narrowedType = new UnionType([new StringType(), new ObjectWithoutClassType()]);
1009-
}
1010-
$result = TypeCombinator::intersect($result, $narrowedType);
1012+
if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1013+
$narrowedType = new UnionType([new ClassStringType(), new ObjectWithoutClassType()]);
1014+
} elseif ((new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1015+
$narrowedType = new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]);
1016+
} else {
1017+
$narrowedType = new UnionType([new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), new ObjectWithoutClassType()]);
10111018
}
1019+
$result = TypeCombinator::intersect($result, $narrowedType);
10121020
}
10131021

10141022
return $result;

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ private static function findTestFiles(): iterable
190190
yield __DIR__ . '/../Rules/Variables/data/bug-7417.php';
191191
yield __DIR__ . '/../Rules/Arrays/data/bug-7469.php';
192192
yield __DIR__ . '/../Rules/Variables/data/bug-3391.php';
193+
yield __DIR__ . '/../Rules/Methods/data/bug-14549.php';
193194

194195
yield __DIR__ . '/../Rules/Functions/data/bug-anonymous-function-method-constant.php';
195196

tests/PHPStan/Analyser/nsrt/bug-3842.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ function testIsArrayOnCallable(callable $value): void {
2222
if (is_array($value)) {
2323
assertType('array<mixed, mixed>&callable(): mixed', $value);
2424
assertType('class-string|object', $value[0]);
25-
assertType('string', $value[1]);
25+
assertType('non-falsy-string', $value[1]);
2626
}
2727
}
2828

2929
/** @param callable-array $value */
3030
function testCallableArrayPhpDoc(array $value): void {
3131
assertType('array&callable(): mixed', $value);
3232
assertType('class-string|object', $value[0]);
33-
assertType('string', $value[1]);
33+
assertType('non-falsy-string', $value[1]);
3434
}
3535

3636
function testIsStringOnCallable(callable $value): void {
@@ -50,7 +50,7 @@ function checkClassString(array $values): void {
5050
/** @param 0|1 $offset */
5151
function testCallableArrayUnionOffset(callable $value, int $offset): void {
5252
if (is_array($value)) {
53-
assertType('object|string', $value[$offset]);
53+
assertType('object|non-falsy-string', $value[$offset]);
5454
}
5555
}
5656

tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,24 @@ public function testBug7662(): void
148148
]);
149149
}
150150

151+
public function testBug14549(): void
152+
{
153+
$this->analyse([__DIR__ . '/data/bug-14549.php'], [
154+
[
155+
'Method Bug14549\Foo::doFoo() has parameter $task with no signature specified for callable.',
156+
12,
157+
],
158+
[
159+
'Method Bug14549\Foo::doIntersection() has parameter $array with no value type specified in iterable type array.',
160+
46,
161+
MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP,
162+
],
163+
[
164+
'Method Bug14549\Foo::doIntersection() has parameter $array with no value type specified in iterable type array.',
165+
46,
166+
MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP,
167+
],
168+
]);
169+
}
170+
151171
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace Bug14549;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
/**
10+
* @param callable-array $task
11+
*/
12+
public function doFoo(array $task): void
13+
{
14+
foreach($task as $k => $v) {
15+
assertType('0|1', $k);
16+
assertType('object|non-falsy-string', $v);
17+
}
18+
assertType('class-string|object', $task[0]);
19+
assertType('non-falsy-string', $task[1]);
20+
}
21+
22+
/**
23+
* @param non-empty-list<string> $list
24+
*/
25+
public function doBar(array $list): void
26+
{
27+
if ($list[0] !== '') {
28+
assertType('non-empty-list<string>&hasOffsetValue(0, non-empty-string)', $list);
29+
30+
if (is_callable($list)) {
31+
assertType('non-empty-list<string>&callable(): mixed&hasOffsetValue(0, non-empty-string)', $list);
32+
assertType('non-empty-string', $list[0]);
33+
assertType('non-falsy-string', $list[1]);
34+
35+
foreach($list as $k => $v) {
36+
assertType('0|1', $k);
37+
assertType('non-falsy-string', $v);
38+
}
39+
}
40+
}
41+
}
42+
43+
/**
44+
* @param (array&callable(array): array) $array
45+
*/
46+
public function doIntersection($array): void
47+
{
48+
}
49+
50+
}
51+
52+

tests/PHPStan/Type/Constant/ConstantStringTypeTest.php

Lines changed: 12 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -185,63 +185,22 @@ public function testSetInvalidValue(): void
185185
$this->assertInstanceOf(ErrorType::class, $result);
186186
}
187187

188-
public static function dataIsDecimalIntegerString(): iterable
188+
#[DataProvider('dataIsCallable')]
189+
public function testIsCallable(TrinaryLogic $trinaryLogic, string $constantValue): void
189190
{
190-
yield [
191-
'0',
192-
TrinaryLogic::createYes(),
193-
];
194-
yield [
195-
'1',
196-
TrinaryLogic::createYes(),
197-
];
198-
yield [
199-
'1234',
200-
TrinaryLogic::createYes(),
201-
];
202-
yield [
203-
'-1',
204-
TrinaryLogic::createYes(),
205-
];
206-
yield [
207-
'+1',
208-
TrinaryLogic::createNo(),
209-
];
210-
yield [
211-
'00',
212-
TrinaryLogic::createNo(),
213-
];
214-
yield [
215-
'01',
216-
TrinaryLogic::createNo(),
217-
];
218-
yield [
219-
'18E+3',
220-
TrinaryLogic::createNo(),
221-
];
222-
yield [
223-
'1.2',
224-
TrinaryLogic::createNo(),
225-
];
226-
yield [
227-
'1,3',
228-
TrinaryLogic::createNo(),
229-
];
230-
yield [
231-
'foo',
232-
TrinaryLogic::createNo(),
233-
];
234-
yield [
235-
'1foo',
236-
TrinaryLogic::createNo(),
237-
];
191+
$this->assertSame(
192+
$trinaryLogic,
193+
(new ConstantStringType($constantValue))->isCallable(),
194+
);
238195
}
239196

240-
#[DataProvider('dataIsDecimalIntegerString')]
241-
public function testIsDecimalIntegerString(string $value, TrinaryLogic $expected): void
197+
public static function dataIsCallable(): iterable
242198
{
243-
$type = new ConstantStringType($value);
244-
$this->assertSame($expected->describe(), $type->isDecimalIntegerString()->describe());
199+
yield [TrinaryLogic::createNo(), ''];
200+
yield [TrinaryLogic::createNo(), '0'];
201+
yield [TrinaryLogic::createYes(), 'substr'];
202+
yield [TrinaryLogic::createYes(), self::class . '::dataIsCallable'];
203+
yield [TrinaryLogic::createMaybe(), self::class . '::methodDoesNotExist'];
245204
}
246205

247206
}

0 commit comments

Comments
 (0)