Skip to content

Commit a69f7ca

Browse files
committed
Merge branch 2.1.x into 2.2.x
2 parents 6191283 + 338b1b6 commit a69f7ca

4 files changed

Lines changed: 284 additions & 0 deletions

File tree

src/Type/IntersectionType.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,17 @@ public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsRes
305305
static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes),
306306
);
307307

308+
// lazyMaxMin can short-circuit to Yes when array<mixed> (inside e.g. array&callable
309+
// or array&hasOffsetValue) is accepted by a specific array type like array<int>,
310+
// because MixedType::isAcceptedBy() always returns Yes. The isSuperTypeOf check
311+
// considers the intersection holistically and catches these false positives.
312+
if ($result->yes()) {
313+
$isSuperType = $acceptingType->isSuperTypeOf($this);
314+
if ($isSuperType->no()) {
315+
return $isSuperType->toAcceptsResult();
316+
}
317+
}
318+
308319
if ($this->isOversizedArray()->yes()) {
309320
if (!$result->no()) {
310321
return AcceptsResult::createYes();

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4049,4 +4049,41 @@ public function testBug13272(): void
40494049
$this->analyse([__DIR__ . '/data/bug-13272.php'], []);
40504050
}
40514051

4052+
public function testBug14549(): void
4053+
{
4054+
$this->checkThisOnly = false;
4055+
$this->checkNullables = true;
4056+
$this->checkUnionTypes = true;
4057+
$this->analyse([__DIR__ . '/data/bug-14549-bis.php'], [
4058+
[
4059+
'Parameter #1 $param of method Bug14549Bis\Foo::callArrayInt() expects array<int>, array&callable given.',
4060+
33,
4061+
],
4062+
[
4063+
'Parameter #1 $param of method Bug14549Bis\Foo::callConstantArrayStringString() expects array{string, string}, array&callable(): mixed given.',
4064+
34,
4065+
],
4066+
[
4067+
'Parameter #1 $param of method Bug14549Bis\Foo::callConstantArrayObjectOrStringStringString() expects array{object|string, string, string}, array&callable(): mixed given.',
4068+
36,
4069+
],
4070+
[
4071+
'Parameter #1 $param of method Bug14549Bis\Foo::callArrayInt() expects array<int>, array&callable given.',
4072+
44,
4073+
],
4074+
[
4075+
'Parameter #1 $param of method Bug14549Bis\Foo::callConstantArrayStringString() expects array{string, string}, array&callable(): mixed given.',
4076+
45,
4077+
],
4078+
[
4079+
'Parameter #1 $param of method Bug14549Bis\Foo::callConstantArrayObjectOrStringStringString() expects array{object|string, string, string}, array&callable(): mixed given.',
4080+
47,
4081+
],
4082+
[
4083+
'Parameter #1 $param of method Bug14549Bis\Foo::callArrayString() expects array<string>, array given.',
4084+
58,
4085+
],
4086+
]);
4087+
}
4088+
40524089
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace Bug14549Bis;
4+
5+
class Foo
6+
{
7+
8+
/** @param array<int> $param */
9+
public function callArrayInt(array $param): void
10+
{
11+
}
12+
13+
/** @param array{string, string} $param */
14+
public function callConstantArrayStringString(array $param): void
15+
{
16+
}
17+
18+
/** @param array{object|string, string} $param */
19+
public function callConstantArrayObjectOrStringString(array $param): void
20+
{
21+
}
22+
23+
/** @param array{object|string, string, string} $param */
24+
public function callConstantArrayObjectOrStringStringString(array $param): void
25+
{
26+
}
27+
28+
/**
29+
* @param callable-array $task
30+
*/
31+
public function doCallWithCallableArray(array $task): void
32+
{
33+
$this->callArrayInt($task);
34+
$this->callConstantArrayStringString($task);
35+
$this->callConstantArrayObjectOrStringString($task);
36+
$this->callConstantArrayObjectOrStringStringString($task);
37+
}
38+
39+
/**
40+
* @param callable&array $task
41+
*/
42+
public function doCallWithCallableAndArray(array $task): void
43+
{
44+
$this->callArrayInt($task);
45+
$this->callConstantArrayStringString($task);
46+
$this->callConstantArrayObjectOrStringString($task);
47+
$this->callConstantArrayObjectOrStringStringString($task);
48+
}
49+
50+
/** @param array<string> $param */
51+
public function callArrayString(array $param): void
52+
{
53+
}
54+
55+
public function doCallWithHasOffsetValue(array $arr): void
56+
{
57+
if (isset($arr[1]) && $arr[1] === 1) {
58+
$this->callArrayString($arr);
59+
$this->callArrayInt($arr);
60+
}
61+
}
62+
63+
}

tests/PHPStan/Type/IntersectionTypeTest.php

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,132 @@ public function testAccepts(Type $type, Type $otherType, TrinaryLogic $expectedR
110110
);
111111
}
112112

113+
/**
114+
* @return Iterator<int, array{Type, Type, TrinaryLogic}>
115+
*/
116+
public static function dataIsAcceptedBy(): Iterator
117+
{
118+
// array&callable isAcceptedBy array - success
119+
yield [
120+
new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]),
121+
new ArrayType(new MixedType(), new MixedType()),
122+
TrinaryLogic::createYes(),
123+
];
124+
125+
// array&callable isAcceptedBy array<int> - failure
126+
yield [
127+
new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]),
128+
new ArrayType(new MixedType(), new IntegerType()),
129+
TrinaryLogic::createNo(),
130+
];
131+
132+
// array&callable isAcceptedBy constantArray{stdClass, string} - maybe
133+
yield [
134+
new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]),
135+
new ConstantArrayType(
136+
[new ConstantIntegerType(0), new ConstantIntegerType(1)],
137+
[new UnionType([new ObjectType('stdClass'), new StringType()]), new StringType()],
138+
),
139+
TrinaryLogic::createMaybe(),
140+
];
141+
142+
// array&callable isAcceptedBy constantArray{string, string} - maybe
143+
yield [
144+
new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]),
145+
new ConstantArrayType(
146+
[new ConstantIntegerType(0), new ConstantIntegerType(1)],
147+
[new StringType(), new StringType()],
148+
),
149+
TrinaryLogic::createMaybe(),
150+
];
151+
152+
// array&hasOffsetValue isAcceptedBy array - success
153+
yield [
154+
new IntersectionType([
155+
new ArrayType(new MixedType(), new MixedType()),
156+
new NonEmptyArrayType(),
157+
new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()),
158+
]),
159+
new ArrayType(new MixedType(), new MixedType()),
160+
TrinaryLogic::createYes(),
161+
];
162+
163+
// array&hasOffsetValue isAcceptedBy array - failure
164+
yield [
165+
new IntersectionType([
166+
new ArrayType(new MixedType(), new MixedType()),
167+
new NonEmptyArrayType(),
168+
new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()),
169+
]),
170+
new ArrayType(new MixedType(), new StringType()),
171+
TrinaryLogic::createNo(),
172+
];
173+
174+
// array&hasOffsetValue isAcceptedBy array<int> - success (matching value type)
175+
yield [
176+
new IntersectionType([
177+
new ArrayType(new MixedType(), new MixedType()),
178+
new NonEmptyArrayType(),
179+
new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()),
180+
]),
181+
new ArrayType(new MixedType(), new IntegerType()),
182+
TrinaryLogic::createYes(),
183+
];
184+
185+
// array&hasOffsetValue isAcceptedBy constantArray{int, int} - success
186+
yield [
187+
new IntersectionType([
188+
new ArrayType(new MixedType(), new MixedType()),
189+
new NonEmptyArrayType(),
190+
new HasOffsetValueType(new ConstantIntegerType(0), new IntegerType()),
191+
]),
192+
new ConstantArrayType(
193+
[new ConstantIntegerType(0), new ConstantIntegerType(1)],
194+
[new IntegerType(), new IntegerType()],
195+
),
196+
TrinaryLogic::createMaybe(),
197+
];
198+
199+
// array&hasOffsetValue isAcceptedBy constantArray{string, string} - failure
200+
yield [
201+
new IntersectionType([
202+
new ArrayType(new MixedType(), new MixedType()),
203+
new NonEmptyArrayType(),
204+
new HasOffsetValueType(new ConstantIntegerType(0), new IntegerType()),
205+
]),
206+
new ConstantArrayType(
207+
[new ConstantIntegerType(0), new ConstantIntegerType(1)],
208+
[new StringType(), new StringType()],
209+
),
210+
TrinaryLogic::createNo(),
211+
];
212+
213+
// array&hasOffsetValue(3, int) isAcceptedBy array<int>|array<string> - yes (array<int> accepts it)
214+
yield [
215+
new IntersectionType([
216+
new ArrayType(new MixedType(), new MixedType()),
217+
new NonEmptyArrayType(),
218+
new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()),
219+
]),
220+
new UnionType([
221+
new ArrayType(new MixedType(), new IntegerType()),
222+
new ArrayType(new MixedType(), new StringType()),
223+
]),
224+
TrinaryLogic::createYes(),
225+
];
226+
}
227+
228+
#[DataProvider('dataIsAcceptedBy')]
229+
public function testIsAcceptedBy(Type $type, Type $acceptingType, TrinaryLogic $expectedResult): void
230+
{
231+
$actualResult = $acceptingType->accepts($type, true)->result;
232+
$this->assertSame(
233+
$expectedResult->describe(),
234+
$actualResult->describe(),
235+
sprintf('%s -> isAcceptedBy(%s)', $type->describe(VerbosityLevel::precise()), $acceptingType->describe(VerbosityLevel::precise())),
236+
);
237+
}
238+
113239
public static function dataIsCallable(): array
114240
{
115241
return [
@@ -363,6 +489,53 @@ public static function dataIsSubTypeOf(): Iterator
363489
]),
364490
TrinaryLogic::createYes(),
365491
];
492+
493+
// array&callable isSubTypeOf array - success
494+
yield [
495+
new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]),
496+
new ArrayType(new MixedType(), new MixedType()),
497+
TrinaryLogic::createYes(),
498+
];
499+
500+
// array&callable isSubTypeOf array<int> - failure
501+
yield [
502+
new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]),
503+
new ArrayType(new MixedType(), new IntegerType()),
504+
TrinaryLogic::createNo(),
505+
];
506+
507+
// array&hasOffsetValue isSubTypeOf array - success
508+
yield [
509+
new IntersectionType([
510+
new ArrayType(new MixedType(), new MixedType()),
511+
new NonEmptyArrayType(),
512+
new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()),
513+
]),
514+
new ArrayType(new MixedType(), new MixedType()),
515+
TrinaryLogic::createYes(),
516+
];
517+
518+
// array&hasOffsetValue isSubTypeOf array<int> - maybe
519+
yield [
520+
new IntersectionType([
521+
new ArrayType(new MixedType(), new MixedType()),
522+
new NonEmptyArrayType(),
523+
new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()),
524+
]),
525+
new ArrayType(new MixedType(), new IntegerType()),
526+
TrinaryLogic::createMaybe(),
527+
];
528+
529+
// array&hasOffsetValue isSubTypeOf array<string> - failure
530+
yield [
531+
new IntersectionType([
532+
new ArrayType(new MixedType(), new MixedType()),
533+
new NonEmptyArrayType(),
534+
new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()),
535+
]),
536+
new ArrayType(new MixedType(), new StringType()),
537+
TrinaryLogic::createNo(),
538+
];
366539
}
367540

368541
#[DataProvider('dataIsSubTypeOf')]

0 commit comments

Comments
 (0)