Skip to content

Commit f499d35

Browse files
committed
Merge remote-tracking branch 'origin/2.1.x' into 2.2.x
2 parents 263f59b + 9743346 commit f499d35

11 files changed

Lines changed: 358 additions & 21 deletions

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;

src/Type/TypeCombinator.php

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -992,7 +992,18 @@ private static function processArrayTypes(array $arrayTypes): array
992992

993993
$reducedArrayTypes = self::optimizeConstantArrays(self::reduceArrays($arrayTypes, true));
994994
foreach ($reducedArrayTypes as $idx => $reducedArray) {
995-
$reducedArrayTypes[$idx] = self::intersect($reducedArray, ...$accessoryTypes);
995+
$applied = $accessoryTypes;
996+
if ($reducedArray->isIterableAtLeastOnce()->no()) {
997+
// Empty arrays cannot satisfy non-empty / oversized constraints —
998+
// applying those accessories would produce a contradictory intersection
999+
// (e.g. `array{}&oversized-array`) that rejects the very value it
1000+
// represents, breaking the super-type contract of the union.
1001+
$applied = array_values(array_filter(
1002+
$applied,
1003+
static fn (Type $t): bool => !($t instanceof OversizedArrayType) && !($t instanceof NonEmptyArrayType),
1004+
));
1005+
}
1006+
$reducedArrayTypes[$idx] = self::intersect($reducedArray, ...$applied);
9961007
}
9971008
return $reducedArrayTypes;
9981009
}
@@ -1089,7 +1100,24 @@ private static function optimizeConstantArrays(array $types): array
10891100
$generalizedKeyType = $innerKeyType->generalize(GeneralizePrecision::moreSpecific());
10901101
$keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType;
10911102

1092-
$generalizedValueType = TypeTraverser::map($innerValueTypes[$i], static function (Type $type) use ($traverse): Type {
1103+
// Inner traversal of the value position. Two subtleties, both
1104+
// of which produced types that failed to be super-types of
1105+
// their contributors:
1106+
// - Empty constant arrays must be left alone; wrapping them
1107+
// builds a contradictory `array{}&oversized-array`.
1108+
// - Fall through via `$innerTraverse`, not the outer
1109+
// `$traverse`. The outer callback fully generalizes a
1110+
// sealed `ConstantArrayType` into `array<intKey, V>&...`,
1111+
// which is correct at the top level but wrong inside a
1112+
// value position: it would treat a sealed `array{a: 1}`
1113+
// reached via `array{}|array{a: 1}` differently from one
1114+
// reached directly, leaving `processArrayTypes` with a
1115+
// mix of shapes it cannot unify cleanly.
1116+
$generalizedValueType = TypeTraverser::map($innerValueTypes[$i], static function (Type $type, callable $innerTraverse): Type {
1117+
if ($type instanceof ConstantArrayType && $type->isIterableAtLeastOnce()->no()) {
1118+
return $type;
1119+
}
1120+
10931121
if ($type instanceof ArrayType || $type instanceof ConstantArrayType) {
10941122
return new IntersectionType([$type, new OversizedArrayType()]);
10951123
}
@@ -1098,7 +1126,7 @@ private static function optimizeConstantArrays(array $types): array
10981126
return $type->generalize(GeneralizePrecision::moreSpecific());
10991127
}
11001128

1101-
return $traverse($type);
1129+
return $innerTraverse($type);
11021130
});
11031131
$valueTypes[$generalizedValueType->describe(VerbosityLevel::precise())] = $generalizedValueType;
11041132
}

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

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
namespace BugYieldOversizedSelfRejectionNsrt;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function build(string $eventClass): array
8+
{
9+
$element = 'Deleted' === $eventClass ? 'old' : 'new';
10+
11+
if (rand()) {
12+
$r = [
13+
'eventClass' => $eventClass,
14+
'changes' => [[$element => [1, '2022-08-04', [['08:00', '12:00']]]]],
15+
'matchedTimechecks' => [],
16+
'invalidates' => [[1, '2022-08-04']],
17+
];
18+
} elseif (rand()) {
19+
$r = [
20+
'eventClass' => $eventClass,
21+
'changes' => [[$element => [1, '2022-08-04', [['08:00', '12:00'], ['14:00', '18:00']]]]],
22+
'matchedTimechecks' => [],
23+
'invalidates' => [[1, '2022-08-04']],
24+
];
25+
} elseif (rand()) {
26+
$r = [
27+
'eventClass' => $eventClass,
28+
'changes' => [[$element => [1, '2022-08-04', [['00:00', '00:00']]]]],
29+
'matchedTimechecks' => [],
30+
'invalidates' => [[1, '2022-08-04']],
31+
];
32+
} elseif (rand()) {
33+
$r = [
34+
'eventClass' => $eventClass,
35+
'changes' => [[$element => [1, '2022-08-04', [['22:00', '04:00']]]]],
36+
'matchedTimechecks' => [],
37+
'invalidates' => [[1, '2022-08-04/2022-08-05']],
38+
];
39+
} elseif (rand()) {
40+
$r = [
41+
'eventClass' => $eventClass,
42+
'changes' => [[$element => [1, '2022-08-04', [['16:00', '23:00']], 'matchedTimecheckIds' => [42]]]],
43+
'matchedTimechecks' => [42 => [1, '2022-08-05T00:05/2022-08-05T02:00']],
44+
'invalidates' => [[1, '2022-08-04/2022-08-05']],
45+
];
46+
} elseif (rand()) {
47+
$r = [
48+
'eventClass' => $eventClass,
49+
'changes' => [
50+
[$element => [1, '2022-08-04', [['08:00', '12:00']]]],
51+
[$element => [1, '2022-08-10', [['08:00', '12:00']]]],
52+
],
53+
'matchedTimechecks' => [],
54+
'invalidates' => [[1, '2022-08-04'], [1, '2022-08-10']],
55+
];
56+
} elseif (rand()) {
57+
$r = [
58+
'eventClass' => $eventClass,
59+
'changes' => [
60+
[$element => [1, '2022-08-04', [['08:00', '12:00']]]],
61+
[$element => [1, '2022-08-05', [['08:00', '12:00']]]],
62+
[$element => [1, '2022-08-06', [['08:00', '12:00']]]],
63+
],
64+
'matchedTimechecks' => [],
65+
'invalidates' => [[1, '2022-08-04/2022-08-06']],
66+
];
67+
} elseif (rand()) {
68+
$r = [
69+
'eventClass' => $eventClass,
70+
'changes' => [
71+
[$element => [1, '2022-08-04', [['08:00', '12:00']]]],
72+
[$element => [2, '2022-08-05', [['08:00', '12:00']]]],
73+
[$element => [2, '2022-08-06', [['08:00', '12:00']]]],
74+
[$element => [3, '2022-08-06', [['08:00', '12:00']]]],
75+
[$element => [3, '2022-08-10', [['08:00', '12:00']]]],
76+
],
77+
'matchedTimechecks' => [],
78+
'invalidates' => [[1, '2022-08-04'], [2, '2022-08-05/2022-08-06'], [3, '2022-08-06'], [3, '2022-08-10']],
79+
];
80+
} else {
81+
$r = [
82+
'eventClass' => $eventClass,
83+
'changes' => [
84+
[$element => [1, '2022-08-04', [['08:00', '12:00']]]],
85+
[$element => [1, '2022-08-05', [['08:00', '12:00']]]],
86+
],
87+
'matchedTimechecks' => [],
88+
'invalidates' => [],
89+
'persistenceEnabled' => false,
90+
];
91+
}
92+
93+
assertType("non-empty-array<literal-string&non-falsy-string, array{}|(array{42: array{1, '2022-08-05T00:05/2022-08-05T02:00'}}&oversized-array)|bool|(list{0: array{1, '2022-08-04'}|non-empty-array{old?: array{0: 1, 1: '2022-08-04', 2: array{array{'16:00', '23:00'}}, matchedTimecheckIds: array{42}}, new?: array{0: 1, 1: '2022-08-04', 2: array{array{'16:00', '23:00'}}, matchedTimecheckIds: array{42}}}|non-empty-array{old?: array{1, '2022-08-04', array{0: array{'08:00', '12:00'}, 1?: array{'14:00', '18:00'}}}, new?: array{1, '2022-08-04', array{0: array{'08:00', '12:00'}, 1?: array{'14:00', '18:00'}}}}|non-empty-array{old?: array{1, '2022-08-04', array{array{'00:00', '00:00'}}}, new?: array{1, '2022-08-04', array{array{'00:00', '00:00'}}}}|non-empty-array{old?: array{1, '2022-08-04', array{array{'22:00', '04:00'}}}, new?: array{1, '2022-08-04', array{array{'22:00', '04:00'}}}}, 1?: array{1, '2022-08-10'}|array{2, '2022-08-05/2022-08-06'}|non-empty-array{old?: array{1, '2022-08-05', array{array{'08:00', '12:00'}}}, new?: array{1, '2022-08-05', array{array{'08:00', '12:00'}}}}|non-empty-array{old?: array{1, '2022-08-10', array{array{'08:00', '12:00'}}}, new?: array{1, '2022-08-10', array{array{'08:00', '12:00'}}}}, 2?: array{3, '2022-08-06'}|non-empty-array{old?: array{1, '2022-08-06', array{array{'08:00', '12:00'}}}, new?: array{1, '2022-08-06', array{array{'08:00', '12:00'}}}}, 3?: array{3, '2022-08-10'}}&oversized-array)|(list{array{1, '2022-08-04/2022-08-05'|'2022-08-04/2022-08-06'}}&oversized-array)|(list{non-empty-array{old?: array{1, '2022-08-04', array{array{'08:00', '12:00'}}}, new?: array{1, '2022-08-04', array{array{'08:00', '12:00'}}}}, non-empty-array{old?: array{2, '2022-08-05', array{array{'08:00', '12:00'}}}, new?: array{2, '2022-08-05', array{array{'08:00', '12:00'}}}}, non-empty-array{old?: array{2, '2022-08-06', array{array{'08:00', '12:00'}}}, new?: array{2, '2022-08-06', array{array{'08:00', '12:00'}}}}, non-empty-array{old?: array{3, '2022-08-06', array{array{'08:00', '12:00'}}}, new?: array{3, '2022-08-06', array{array{'08:00', '12:00'}}}}, non-empty-array{old?: array{3, '2022-08-10', array{array{'08:00', '12:00'}}}, new?: array{3, '2022-08-10', array{array{'08:00', '12:00'}}}}}&oversized-array)|string>&oversized-array", $r);
94+
95+
return $r;
96+
}

tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,9 @@ public function testBug7484(): void
8282
]);
8383
}
8484

85+
public function testYieldOversizedSelfRejection(): void
86+
{
87+
$this->analyse([__DIR__ . '/data/bug-yield-oversized-self-rejection.php'], []);
88+
}
89+
8590
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace BugYieldOversizedSelfRejection;
4+
5+
use Generator;
6+
7+
/**
8+
* @param callable(string): iterable<string, mixed> $fn
9+
* @return Generator<string, mixed>
10+
*/
11+
function scollect(callable $fn): Generator
12+
{
13+
foreach (['Created', 'Deleted'] as $eventClass) {
14+
yield from $fn($eventClass);
15+
}
16+
}
17+
18+
scollect(static function (string $eventClass): iterable {
19+
$element = 'Deleted' === $eventClass ? 'old' : 'new';
20+
21+
yield '1' => [
22+
'eventClass' => $eventClass,
23+
'changes' => [[$element => [1, '2022-08-04', [['08:00', '12:00']]]]],
24+
'matchedTimechecks' => [],
25+
'invalidates' => [[1, '2022-08-04']],
26+
];
27+
yield '2' => [
28+
'eventClass' => $eventClass,
29+
'changes' => [[$element => [1, '2022-08-04', [['08:00', '12:00'], ['14:00', '18:00']]]]],
30+
'matchedTimechecks' => [],
31+
'invalidates' => [[1, '2022-08-04']],
32+
];
33+
yield '3' => [
34+
'eventClass' => $eventClass,
35+
'changes' => [[$element => [1, '2022-08-04', [['00:00', '00:00']]]]],
36+
'matchedTimechecks' => [],
37+
'invalidates' => [[1, '2022-08-04']],
38+
];
39+
yield '4' => [
40+
'eventClass' => $eventClass,
41+
'changes' => [[$element => [1, '2022-08-04', [['22:00', '04:00']]]]],
42+
'matchedTimechecks' => [],
43+
'invalidates' => [[1, '2022-08-04/2022-08-05']],
44+
];
45+
yield '5' => [
46+
'eventClass' => $eventClass,
47+
'changes' => [[$element => [1, '2022-08-04', [['16:00', '23:00']], 'matchedTimecheckIds' => [42]]]],
48+
'matchedTimechecks' => [42 => [1, '2022-08-05T00:05/2022-08-05T02:00']],
49+
'invalidates' => [[1, '2022-08-04/2022-08-05']],
50+
];
51+
yield '6' => [
52+
'eventClass' => $eventClass,
53+
'changes' => [
54+
[$element => [1, '2022-08-04', [['08:00', '12:00']]]],
55+
[$element => [1, '2022-08-10', [['08:00', '12:00']]]],
56+
],
57+
'matchedTimechecks' => [],
58+
'invalidates' => [[1, '2022-08-04'], [1, '2022-08-10']],
59+
];
60+
yield '7' => [
61+
'eventClass' => $eventClass,
62+
'changes' => [
63+
[$element => [1, '2022-08-04', [['08:00', '12:00']]]],
64+
[$element => [1, '2022-08-05', [['08:00', '12:00']]]],
65+
[$element => [1, '2022-08-06', [['08:00', '12:00']]]],
66+
],
67+
'matchedTimechecks' => [],
68+
'invalidates' => [[1, '2022-08-04/2022-08-06']],
69+
];
70+
yield '8' => [
71+
'eventClass' => $eventClass,
72+
'changes' => [
73+
[$element => [1, '2022-08-04', [['08:00', '12:00']]]],
74+
[$element => [2, '2022-08-05', [['08:00', '12:00']]]],
75+
[$element => [2, '2022-08-06', [['08:00', '12:00']]]],
76+
[$element => [3, '2022-08-06', [['08:00', '12:00']]]],
77+
[$element => [3, '2022-08-10', [['08:00', '12:00']]]],
78+
],
79+
'matchedTimechecks' => [],
80+
'invalidates' => [[1, '2022-08-04'], [2, '2022-08-05/2022-08-06'], [3, '2022-08-06'], [3, '2022-08-10']],
81+
];
82+
yield '9' => [
83+
'eventClass' => $eventClass,
84+
'changes' => [
85+
[$element => [1, '2022-08-04', [['08:00', '12:00']]]],
86+
[$element => [1, '2022-08-05', [['08:00', '12:00']]]],
87+
],
88+
'matchedTimechecks' => [],
89+
'invalidates' => [],
90+
'persistenceEnabled' => false,
91+
];
92+
});

0 commit comments

Comments
 (0)