Skip to content

Commit fe05703

Browse files
authored
Merge branch refs/heads/2.1.x into 2.2.x
2 parents f3b36e6 + ae822a8 commit fe05703

File tree

4 files changed

+306
-5
lines changed

4 files changed

+306
-5
lines changed

src/Type/Constant/ConstantArrayType.php

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,14 @@ public function unsetOffset(Type $offsetType): Type
741741
$k++;
742742
}
743743

744-
return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys, TrinaryLogic::createNo());
744+
$newIsList = self::isListAfterUnset(
745+
$newKeyTypes,
746+
$newOptionalKeys,
747+
$this->isList,
748+
in_array($i, $this->optionalKeys, true),
749+
);
750+
751+
return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys, $newIsList);
745752
}
746753

747754
return $this;
@@ -751,6 +758,7 @@ public function unsetOffset(Type $offsetType): Type
751758
if (count($constantScalars) > 0) {
752759
$optionalKeys = $this->optionalKeys;
753760

761+
$arrayHasChanged = false;
754762
foreach ($constantScalars as $constantScalar) {
755763
$constantScalar = $constantScalar->toArrayKey();
756764
if (!$constantScalar instanceof ConstantIntegerType && !$constantScalar instanceof ConstantStringType) {
@@ -762,6 +770,7 @@ public function unsetOffset(Type $offsetType): Type
762770
continue;
763771
}
764772

773+
$arrayHasChanged = true;
765774
if (in_array($i, $optionalKeys, true)) {
766775
continue 2;
767776
}
@@ -770,21 +779,77 @@ public function unsetOffset(Type $offsetType): Type
770779
}
771780
}
772781

773-
return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, TrinaryLogic::createNo());
782+
if (!$arrayHasChanged) {
783+
return $this;
784+
}
785+
786+
$newIsList = self::isListAfterUnset(
787+
$this->keyTypes,
788+
$optionalKeys,
789+
$this->isList,
790+
count($optionalKeys) === count($this->optionalKeys),
791+
);
792+
793+
return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $newIsList);
774794
}
775795

776796
$optionalKeys = $this->optionalKeys;
777-
$isList = $this->isList;
797+
$arrayHasChanged = false;
778798
foreach ($this->keyTypes as $i => $keyType) {
779799
if (!$offsetType->isSuperTypeOf($keyType)->yes()) {
780800
continue;
781801
}
802+
$arrayHasChanged = true;
782803
$optionalKeys[] = $i;
783-
$isList = TrinaryLogic::createNo();
784804
}
785805
$optionalKeys = array_values(array_unique($optionalKeys));
786806

787-
return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $isList);
807+
if (!$arrayHasChanged) {
808+
return $this;
809+
}
810+
811+
$newIsList = self::isListAfterUnset(
812+
$this->keyTypes,
813+
$optionalKeys,
814+
$this->isList,
815+
count($optionalKeys) === count($this->optionalKeys),
816+
);
817+
818+
return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $newIsList);
819+
}
820+
821+
/**
822+
* When we're unsetting something not on the array, it will be untouched,
823+
* So the nextAutoIndexes won't change, and the array might still be a list even with PHPStan definition.
824+
*
825+
* @param list<ConstantIntegerType|ConstantStringType> $newKeyTypes
826+
* @param int[] $newOptionalKeys
827+
*/
828+
private static function isListAfterUnset(array $newKeyTypes, array $newOptionalKeys, TrinaryLogic $arrayIsList, bool $unsetOptionalKey): TrinaryLogic
829+
{
830+
if (!$unsetOptionalKey || $arrayIsList->no()) {
831+
return TrinaryLogic::createNo();
832+
}
833+
834+
$isListOnlyIfKeysAreOptional = false;
835+
foreach ($newKeyTypes as $k2 => $newKeyType2) {
836+
if (!$newKeyType2 instanceof ConstantIntegerType || $newKeyType2->getValue() !== $k2) {
837+
// We found a non-optional key that implies that the array is never a list.
838+
if (!in_array($k2, $newOptionalKeys, true)) {
839+
return TrinaryLogic::createNo();
840+
}
841+
842+
// The array can still be a list if all the following keys are also optional.
843+
$isListOnlyIfKeysAreOptional = true;
844+
continue;
845+
}
846+
847+
if ($isListOnlyIfKeysAreOptional && !in_array($k2, $newOptionalKeys, true)) {
848+
return TrinaryLogic::createNo();
849+
}
850+
}
851+
852+
return TrinaryLogic::createMaybe();
788853
}
789854

790855
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14177Nsrt;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
/**
10+
* @param list{0: string, 1: string, 2?: string, 3?: string} $b
11+
*/
12+
public function testList(array $b): void
13+
{
14+
if (array_key_exists(3, $b)) {
15+
assertType('list{0: string, 1: string, 2?: string, 3: string}', $b);
16+
} else {
17+
assertType('list{0: string, 1: string, 2?: string}', $b);
18+
}
19+
assertType('list{0: string, 1: string, 2?: string, 3?: string}', $b);
20+
}
21+
22+
public function placeholderToEditor(string $html): void
23+
{
24+
$result = preg_replace_callback(
25+
'~\[image\\sid="(\\d+)"(?:\\shref="([^"]*)")?(?:\\sclass="([^"]*)")?\]~',
26+
function (array $matches): string {
27+
$id = (int) $matches[1];
28+
29+
assertType('list{0: non-falsy-string, 1: numeric-string, 2?: string, 3?: string}', $matches);
30+
31+
$replacement = sprintf(
32+
'<img src="%s"%s/>',
33+
$id,
34+
array_key_exists(3, $matches) ? sprintf(' class="%s"', $matches[3]) : '',
35+
);
36+
37+
assertType('list{0: non-falsy-string, 1: numeric-string, 2?: string, 3?: string}', $matches);
38+
39+
return array_key_exists(2, $matches) && $matches[2] !== ''
40+
? sprintf('<a href="%s">%s</a>', $matches[2], $replacement)
41+
: $replacement;
42+
},
43+
$html,
44+
);
45+
}
46+
47+
public function placeholderToEditor2(string $html): void
48+
{
49+
$result = preg_replace_callback(
50+
'~\[image\\sid="(\\d+)?"(?:\\shref="([^"]*)")?(?:\\sclass="([^"]*)")?\]~',
51+
function (array $matches): string {
52+
$id = (int) $matches[0];
53+
54+
assertType('list{0: non-falsy-string, 1?: \'\'|numeric-string, 2?: string, 3?: string}', $matches);
55+
56+
$replacement = sprintf(
57+
'<img src="%s"%s/>',
58+
$id,
59+
array_key_exists(2, $matches) ? sprintf(' class="%s"', $matches[2]) : '',
60+
);
61+
62+
assertType('list{0: non-falsy-string, 1?: \'\'|numeric-string, 2?: string, 3?: string}', $matches);
63+
64+
return array_key_exists(1, $matches) && $matches[1] !== ''
65+
? sprintf('<a href="%s">%s</a>', $matches[1], $replacement)
66+
: $replacement;
67+
},
68+
$html,
69+
);
70+
}
71+
}
72+
73+
class HelloWorld2
74+
{
75+
/**
76+
* @param list{0: string, 1: string, 2?: string, 3?: string} $b
77+
*/
78+
public function testUnset0OnList(array $b): void
79+
{
80+
assertType('true', array_is_list($b));
81+
unset($b[0]);
82+
assertType('false', array_is_list($b));
83+
$b[] = 'foo';
84+
assertType('false', array_is_list($b));
85+
}
86+
87+
/**
88+
* @param list{0: string, 1: string, 2?: string, 3?: string} $b
89+
*/
90+
public function testUnset1OnList(array $b): void
91+
{
92+
assertType('true', array_is_list($b));
93+
unset($b[1]);
94+
assertType('false', array_is_list($b));
95+
$b[] = 'foo';
96+
assertType('false', array_is_list($b));
97+
}
98+
99+
/**
100+
* @param list{0: string, 1: string, 2?: string, 3?: string} $b
101+
*/
102+
public function testUnset2OnList(array $b): void
103+
{
104+
assertType('true', array_is_list($b));
105+
unset($b[2]);
106+
assertType('bool', array_is_list($b));
107+
$b[] = 'foo';
108+
assertType('bool', array_is_list($b));
109+
}
110+
111+
/**
112+
* @param list{0: string, 1: string, 2?: string, 3?: string} $b
113+
*/
114+
public function testUnset3OnList(array $b): void
115+
{
116+
assertType('true', array_is_list($b));
117+
unset($b[3]);
118+
assertType('bool', array_is_list($b));
119+
$b[] = 'foo';
120+
assertType('bool', array_is_list($b));
121+
}
122+
123+
/**
124+
* @param array{0: string, 1?: string, 2: string, 3?: string} $b
125+
*/
126+
public function testUnset0OnArray(array $b): void
127+
{
128+
assertType('bool', array_is_list($b));
129+
unset($b[0]);
130+
assertType('false', array_is_list($b));
131+
$b[] = 'foo';
132+
assertType('false', array_is_list($b));
133+
}
134+
135+
/**
136+
* @param array{0: string, 1?: string, 2: string, 3?: string} $b
137+
*/
138+
public function testUnset1OnArray(array $b): void
139+
{
140+
assertType('bool', array_is_list($b));
141+
unset($b[1]);
142+
assertType('false', array_is_list($b));
143+
$b[] = 'foo';
144+
assertType('false', array_is_list($b));
145+
}
146+
147+
/**
148+
* @param array{0: string, 1?: string, 2: string, 3?: string} $b
149+
*/
150+
public function testUnset2OnArray(array $b): void
151+
{
152+
assertType('bool', array_is_list($b));
153+
unset($b[2]);
154+
assertType('false', array_is_list($b));
155+
$b[] = 'foo';
156+
assertType('false', array_is_list($b));
157+
}
158+
159+
/**
160+
* @param array{0: string, 1?: string, 2: string, 3?: string} $b
161+
*/
162+
public function testUnset3OnArray(array $b): void
163+
{
164+
assertType('bool', array_is_list($b));
165+
unset($b[3]);
166+
assertType('bool', array_is_list($b));
167+
$b[] = 'foo';
168+
assertType('bool', array_is_list($b));
169+
}
170+
171+
/**
172+
* @param list{0: string, 1: string, 2?: string, 3?: string} $a
173+
* @param list{0: string, 1?: string, 2?: string, 3?: string} $b
174+
* @param list{0: string, 1?: string, 2?: string, 3: string} $c
175+
* @param 1|2 $int
176+
*/
177+
public function testUnsetNonConstant(array $a, array $b, array $c, int $int): void
178+
{
179+
assertType('true', array_is_list($a));
180+
assertType('true', array_is_list($b));
181+
assertType('true', array_is_list($c));
182+
unset($a[$int]);
183+
unset($b[$int]);
184+
unset($c[$int]);
185+
assertType('false', array_is_list($a));
186+
assertType('bool', array_is_list($b));
187+
assertType('bool', array_is_list($c));
188+
}
189+
190+
/**
191+
* @param list{0?: string, 1?: string, 2?: string, 3?: string} $a
192+
* @param list{0: string, 1?: string, 2?: string, 3?: string} $b
193+
*/
194+
public function testUnsetInt(array $a, array $b, array $c, int $int): void
195+
{
196+
assertType('true', array_is_list($a));
197+
assertType('true', array_is_list($b));
198+
unset($a[$int]);
199+
unset($b[$int]);
200+
assertType('bool', array_is_list($a));
201+
assertType('false', array_is_list($b));
202+
}
203+
}

tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,4 +1151,10 @@ public function testPr4375(): void
11511151
$this->analyse([__DIR__ . '/data/pr-4375.php'], []);
11521152
}
11531153

1154+
public function testBug14177(): void
1155+
{
1156+
$this->treatPhpDocTypesAsCertain = true;
1157+
$this->analyse([__DIR__ . '/data/bug-14177.php'], []);
1158+
}
1159+
11541160
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14177;
4+
5+
class HelloWorld
6+
{
7+
public function placeholderToEditor(string $html): void
8+
{
9+
$result = preg_replace_callback(
10+
'~\[image\\sid="(\\d+)"(?:\\shref="([^"]*)")?(?:\\sclass="([^"]*)")?]~',
11+
function (array $matches): string {
12+
$id = (int) $matches[1];
13+
14+
$replacement = sprintf(
15+
'<img src="%s"%s/>',
16+
$id,
17+
array_key_exists(3, $matches) ? sprintf(' class="%s"', $matches[3]) : '',
18+
);
19+
20+
return array_key_exists(2, $matches) && $matches[2] !== ''
21+
? sprintf('<a href="%s">%s</a>', $matches[2], $replacement)
22+
: $replacement;
23+
},
24+
$html,
25+
);
26+
}
27+
}

0 commit comments

Comments
 (0)