Skip to content

Commit 61cdf12

Browse files
phpstan-botgithub-actions[bot]claudeVincentLanglet
authored
Fix phpstan/phpstan#12665: should return array{a: string, b: int, c: int} but returns non-empty-array<'a'|'b'|'c', int|string>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Vincent Langlet <vincentlanglet@hotmail.fr>
1 parent a5be566 commit 61cdf12

File tree

11 files changed

+215
-2
lines changed

11 files changed

+215
-2
lines changed

src/Type/Constant/ConstantArrayTypeBuilder.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
258258
}
259259
if (count($scalarTypes) > 0 && count($scalarTypes) < self::ARRAY_COUNT_LIMIT) {
260260
$match = true;
261+
$hasMatch = false;
261262
$valueTypes = $this->valueTypes;
262263
foreach ($scalarTypes as $scalarType) {
263264
$offsetMatch = false;
@@ -273,6 +274,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
273274
}
274275

275276
if ($offsetMatch) {
277+
$hasMatch = true;
276278
continue;
277279
}
278280

@@ -283,6 +285,26 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
283285
$this->valueTypes = $valueTypes;
284286
return;
285287
}
288+
289+
if (!$hasMatch && count($this->keyTypes) > 0) {
290+
foreach ($scalarTypes as $scalarType) {
291+
$this->keyTypes[] = $scalarType;
292+
$this->valueTypes[] = $valueType;
293+
$this->optionalKeys[] = count($this->keyTypes) - 1;
294+
}
295+
296+
$this->isList = TrinaryLogic::createNo();
297+
298+
if (
299+
!$this->disableArrayDegradation
300+
&& count($this->keyTypes) > self::ARRAY_COUNT_LIMIT
301+
) {
302+
$this->degradeToGeneralArray = true;
303+
$this->oversized = true;
304+
}
305+
306+
return;
307+
}
286308
}
287309

288310
$this->isList = TrinaryLogic::createNo();

tests/PHPStan/Analyser/nsrt/array-fill-keys.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,14 @@ function withObjectKey() : array
5454
function withUnionKeys(): void
5555
{
5656
$arr1 = ['foo', rand(0, 1) ? 'bar1' : 'bar2', 'baz'];
57-
assertType("non-empty-array<'bar1'|'bar2'|'baz'|'foo', 'b'>", array_fill_keys($arr1, 'b'));
57+
assertType("array{foo: 'b', bar1?: 'b', bar2?: 'b', baz: 'b'}", array_fill_keys($arr1, 'b'));
5858

5959
$arr2 = ['foo'];
6060
if (rand(0, 1)) {
6161
$arr2[] = 'bar';
6262
}
6363
$arr2[] = 'baz';
64-
assertType("non-empty-array<'bar'|'baz'|'foo', 'b'>", array_fill_keys($arr2, 'b'));
64+
assertType("array{foo: 'b', bar?: 'b', baz?: 'b'}", array_fill_keys($arr2, 'b'));
6565
}
6666

6767
function withOptionalKeys(): void
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug12665;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Broken
8+
{
9+
/** @return array{a: string, b: int, c: int} */
10+
public function break(string $s, int $i): array
11+
{
12+
$array = ['a' => $s];
13+
foreach (['b', 'c'] as $letter) {
14+
$array[$letter] = $i;
15+
}
16+
assertType('array{a: string, b?: int, c?: int}', $array);
17+
return $array;
18+
}
19+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug9907;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
/**
10+
* @param 'foo'|'bar' $key
11+
*/
12+
public function sayHello(string $key): void
13+
{
14+
$a = ['id' => null, $key => 'string'];
15+
16+
assertType("array{id: null, foo?: 'string', bar?: 'string'}", $a);
17+
}
18+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace SetConstantUnionOffsetOnConstantArray;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @param array{foo: int} $a
12+
*/
13+
public function doFoo(array $a): void
14+
{
15+
$k = rand(0, 1) ? 'a' : 'b';
16+
$a[$k] = 256;
17+
assertType('array{foo: int, a?: 256, b?: 256}', $a);
18+
}
19+
20+
}

tests/PHPStan/Rules/Classes/InstantiationRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,4 +609,10 @@ public function testBug14251(): void
609609
]);
610610
}
611611

612+
#[RequiresPhp('>= 8.0')]
613+
public function testBug11006(): void
614+
{
615+
$this->analyse([__DIR__ . '/data/bug-11006.php'], []);
616+
}
617+
612618
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug11006;
6+
7+
class ProductParentPayloadDto
8+
{
9+
/** @param null|'size_uk'|'size_us' $SizeAttributeCode */
10+
public function __construct(
11+
public ?string $SizeAttributeCode,
12+
) {
13+
}
14+
}
15+
16+
class AkeneoUpdateProductDto
17+
{
18+
/**
19+
* @param array{
20+
* ean?: array<StringOrNullAttributeDto>,
21+
* osa_sizes?: array<StringAttributeDto>,
22+
* size_uk?: array<StringOrNullAttributeDto>,
23+
* size_us?: array<StringOrNullAttributeDto>,
24+
* } $values
25+
*/
26+
public function __construct(
27+
public array $values,
28+
) {
29+
}
30+
}
31+
32+
class StringOrNullAttributeDto
33+
{
34+
public function __construct(
35+
public ?string $data,
36+
) {
37+
}
38+
}
39+
40+
class StringAttributeDto
41+
{
42+
public function __construct(
43+
public string $data,
44+
) {
45+
}
46+
}
47+
48+
49+
class PhpStanProblem
50+
{
51+
public function example(ProductParentPayloadDto $productParentPayloadDto): void
52+
{
53+
if (null === $productParentPayloadDto->SizeAttributeCode) {
54+
return;
55+
}
56+
57+
$values = [
58+
'ean' => [
59+
new StringOrNullAttributeDto(''),
60+
],
61+
$productParentPayloadDto->SizeAttributeCode => [
62+
new StringOrNullAttributeDto(''),
63+
],
64+
// This part goes wrong
65+
'osa_sizes' => [
66+
new StringAttributeDto(''),
67+
],
68+
];
69+
70+
$productData = new AkeneoUpdateProductDto(
71+
values: $values,
72+
);
73+
}
74+
}

tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,9 @@ public function testBug1889(): void
137137
]);
138138
}
139139

140+
public function testBug8774(): void
141+
{
142+
$this->analyse([__DIR__ . '/data/bug-8774.php'], []);
143+
}
144+
140145
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug8774;
4+
5+
class ModerateCtrl
6+
{
7+
private const DISABLE_KEYS_AND_LABELS = [
8+
'DisablePosting' => 'Posting on forum and comments',
9+
'DisableAvatar' => 'Avatar and Custom Icon',
10+
];
11+
12+
public static function handleModerate(): void
13+
{
14+
$summaryTemplates = [
15+
'PermissionID' => "Class changed from <b>'%s'</b> to <b>'%s'</b>.",
16+
'Reset' => '%s reset.',
17+
];
18+
19+
foreach (self::DISABLE_KEYS_AND_LABELS as $key => $label) {
20+
$summaryTemplates[$key] = "Disable $label status %s.";
21+
}
22+
23+
echo sprintf($summaryTemplates['Reset'], 'foo');
24+
}
25+
}

tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,4 +821,9 @@ public function testBug10595(): void
821821
$this->analyse([__DIR__ . '/data/bug-10595.php'], []);
822822
}
823823

824+
public function testBug14080(): void
825+
{
826+
$this->analyse([__DIR__ . '/data/bug-14080.php'], []);
827+
}
828+
824829
}

0 commit comments

Comments
 (0)