Skip to content

Commit 1f9f463

Browse files
committed
Narrow type on setting offsets of properties
1 parent b614f70 commit 1f9f463

9 files changed

+153
-65
lines changed

src/Analyser/MutatingScope.php

-10
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
3535
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
3636
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
37-
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
3837
use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr;
3938
use PHPStan\Node\Expr\PropertyInitializationExpr;
4039
use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
@@ -687,15 +686,6 @@ public function getType(Expr $node): Type
687686
return $node->getExprType();
688687
}
689688

690-
if ($node instanceof OriginalPropertyTypeExpr) {
691-
$propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node->getPropertyFetch(), $this);
692-
if ($propertyReflection === null) {
693-
return new ErrorType();
694-
}
695-
696-
return $propertyReflection->getReadableType();
697-
}
698-
699689
$key = $this->getNodeKey($node);
700690

701691
if (!array_key_exists($key, $this->resolvedTypes)) {

src/Analyser/NodeScopeResolver.php

+2-11
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@
8585
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
8686
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
8787
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
88-
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
8988
use PHPStan\Node\Expr\PropertyInitializationExpr;
9089
use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
9190
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
@@ -5346,12 +5345,8 @@ private function processAssignVar(
53465345
$originalVar = $var;
53475346
$assignedPropertyExpr = $assignedExpr;
53485347
while ($var instanceof ArrayDimFetch) {
5349-
$varForSetOffsetValue = $var->var;
5350-
if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) {
5351-
$varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue);
5352-
}
53535348
$assignedPropertyExpr = new SetOffsetValueTypeExpr(
5354-
$varForSetOffsetValue,
5349+
$var->var,
53555350
$var->dim,
53565351
$assignedPropertyExpr,
53575352
);
@@ -5673,12 +5668,8 @@ static function (): void {
56735668
$dimFetchStack = [];
56745669
$assignedPropertyExpr = $assignedExpr;
56755670
while ($var instanceof ExistingArrayDimFetch) {
5676-
$varForSetOffsetValue = $var->getVar();
5677-
if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) {
5678-
$varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue);
5679-
}
56805671
$assignedPropertyExpr = new SetExistingOffsetValueTypeExpr(
5681-
$varForSetOffsetValue,
5672+
$var->getVar(),
56825673
$var->getDim(),
56835674
$assignedPropertyExpr,
56845675
);

src/Node/Printer/Printer.php

-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
99
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
1010
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
11-
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
1211
use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr;
1312
use PHPStan\Node\Expr\PropertyInitializationExpr;
1413
use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
@@ -55,11 +54,6 @@ protected function pPHPStan_Node_ExistingArrayDimFetch(ExistingArrayDimFetch $ex
5554
return sprintf('__phpstanExistingArrayDimFetch(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim()));
5655
}
5756

58-
protected function pPHPStan_Node_OriginalPropertyTypeExpr(OriginalPropertyTypeExpr $expr): string // phpcs:ignore
59-
{
60-
return sprintf('__phpstanOriginalPropertyType(%s)', $this->p($expr->getPropertyFetch()));
61-
}
62-
6357
protected function pPHPStan_Node_SetOffsetValueTypeExpr(SetOffsetValueTypeExpr $expr): string // phpcs:ignore
6458
{
6559
return sprintf('__phpstanSetOffsetValueType(%s, %s, %s)', $this->p($expr->getVar()), $expr->getDim() !== null ? $this->p($expr->getDim()) : 'null', $this->p($expr->getValue()));

tests/PHPStan/Rules/Arrays/data/appended-array-item.php

+16-4
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,28 @@ class Foo
1111
/** @var callable[] */
1212
private $callables;
1313

14-
public function doFoo()
14+
public function accepted()
1515
{
1616
$this->integers[] = 4;
1717
$this->integers['foo'] = 5;
18-
$this->integers[] = 'foo';
19-
$this->callables[] = [$this, 'doFoo'];
20-
$this->callables[] = [1, 2, 3];
2118
$this->callables[] = ['Closure', 'bind'];
2219
$this->callables[] = 'strpos';
20+
$this->callables[] = [$this, 'accepted'];
21+
}
22+
23+
public function notAccepted1()
24+
{
25+
$this->integers[] = 'foo';
26+
$this->callables[] = [1, 2, 3];
27+
}
28+
29+
public function notAccepted2()
30+
{
2331
$this->callables[] = [__CLASS__, 'classMethod'];
32+
}
33+
34+
public function notAccepted3()
35+
{
2436
$world = 'world';
2537
$this->callables[] = ['Foo', "Hello $world"];
2638

tests/PHPStan/Rules/Arrays/data/appended-array-key.php

+19-13
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,34 @@ class Foo
1414
/** @var array<int|string, mixed> */
1515
private $bothArray;
1616

17-
/**
18-
* @param int|string $intOrString
19-
*/
20-
public function doFoo(
21-
int $int,
22-
string $string,
23-
$intOrString,
24-
?string $stringOrNull
25-
)
17+
public function invalid()
2618
{
2719
$this->intArray[new \DateTimeImmutable()] = 1;
28-
$this->intArray[$intOrString] = 1;
29-
$this->intArray[$int] = 1;
20+
}
21+
22+
/** @param int|string $intOrString */
23+
public function notAccepted(int $int, string $string, $intOrString)
24+
{
3025
$this->intArray[$string] = 1;
26+
$this->intArray[$intOrString] = 1;
3127
$this->stringArray[$int] = 1;
32-
$this->stringArray[$string] = 1;
3328
$this->stringArray[$intOrString] = 1;
29+
}
30+
31+
public function notAcceptedStringIntCase()
32+
{
33+
$this->stringArray['0'] = 1;
34+
}
35+
36+
/** @param int|string $intOrString */
37+
public function accepted(int $int, string $string, $intOrString, ?string $stringOrNull)
38+
{
39+
$this->intArray[$int] = 1;
40+
$this->stringArray[$string] = 1;
3441
$this->bothArray[$int] = 1;
3542
$this->bothArray[$intOrString] = 1;
3643
$this->bothArray[$string] = 1;
3744
$this->stringArray[$stringOrNull] = 1; // will be cast to string
38-
$this->stringArray['0'] = 1;
3945
}
4046

4147
public function checkRewrittenArray()

tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php

+39-21
Original file line numberDiff line numberDiff line change
@@ -253,35 +253,35 @@ public function testAppendendArrayKey(): void
253253
$this->analyse([__DIR__ . '/../Arrays/data/appended-array-key.php'], [
254254
[
255255
'Property AppendedArrayKey\Foo::$intArray (array<int, mixed>) does not accept array<int|string, mixed>.',
256-
28,
256+
25,
257257
],
258258
[
259259
'Property AppendedArrayKey\Foo::$intArray (array<int, mixed>) does not accept array<int|string, mixed>.',
260-
30,
260+
26,
261261
],
262262
[
263263
'Property AppendedArrayKey\Foo::$stringArray (array<string, mixed>) does not accept array<int|string, mixed>.',
264-
31,
264+
27,
265265
],
266266
[
267267
'Property AppendedArrayKey\Foo::$stringArray (array<string, mixed>) does not accept array<int|string, mixed>.',
268-
33,
268+
28,
269269
],
270270
[
271271
'Property AppendedArrayKey\Foo::$stringArray (array<string, mixed>) does not accept array<int|string, mixed>.',
272-
38,
272+
33,
273273
],
274274
[
275-
'Property AppendedArrayKey\Foo::$stringArray (array<string, mixed>) does not accept array<int|string, mixed>.',
276-
46,
275+
'Property AppendedArrayKey\Foo::$stringArray (array<string, mixed>) does not accept array<int, false>.',
276+
52,
277277
],
278278
[
279279
'Property AppendedArrayKey\MorePreciseKey::$test (array<1|2|3, string>) does not accept non-empty-array<int, string>.',
280-
80,
280+
86,
281281
],
282282
[
283283
'Property AppendedArrayKey\MorePreciseKey::$test (array<1|2|3, string>) does not accept non-empty-array<1|2|3|4, string>.',
284-
85,
284+
91,
285285
],
286286
]);
287287
}
@@ -303,35 +303,35 @@ public function testAppendedArrayItemType(): void
303303
[
304304
[
305305
'Property AppendedArrayItem\Foo::$integers (array<int>) does not accept array<int|string>.',
306-
18,
306+
25,
307307
],
308308
[
309309
'Property AppendedArrayItem\Foo::$callables (array<callable(): mixed>) does not accept non-empty-array<array{1, 2, 3}|(callable(): mixed)>.',
310-
20,
310+
26,
311311
],
312312
[
313313
'Property AppendedArrayItem\Foo::$callables (array<callable(): mixed>) does not accept non-empty-array<array{\'AppendedArrayItem\\\\Foo\', \'classMethod\'}|(callable(): mixed)>.',
314-
23,
314+
31,
315315
],
316316
[
317317
'Property AppendedArrayItem\Foo::$callables (array<callable(): mixed>) does not accept non-empty-array<array{\'Foo\', \'Hello world\'}|(callable(): mixed)>.',
318-
25,
318+
37,
319319
],
320320
[
321321
'Property AppendedArrayItem\Foo::$integers (array<int>) does not accept array<int|string>.',
322-
27,
322+
39,
323323
],
324324
[
325325
'Property AppendedArrayItem\Foo::$integers (array<int>) does not accept array<int|string>.',
326-
32,
326+
44,
327327
],
328328
[
329329
'Property AppendedArrayItem\Bar::$stringCallables (array<callable(): string>) does not accept non-empty-array<(callable(): string)|(Closure(): 1)>.',
330-
45,
330+
57,
331331
],
332332
[
333333
'Property AppendedArrayItem\Baz::$staticProperty (array<AppendedArrayItem\Lorem>) does not accept array<AppendedArrayItem\Baz>.',
334-
79,
334+
91,
335335
],
336336
],
337337
);
@@ -355,7 +355,7 @@ public function testBug6286(): void
355355
{
356356
$this->analyse([__DIR__ . '/data/bug-6286.php'], [
357357
[
358-
'Property Bug6286\HelloWorld::$details (array{name: string, age: int}) does not accept array{name: string, age: \'Forty-two\'}.',
358+
'Property Bug6286\HelloWorld::$details (array{name: string, age: int}) does not accept array{name: \'Douglas Adams\', age: \'Forty-two\'}.',
359359
19,
360360
"Offset 'age' (int) does not accept type string.",
361361
],
@@ -389,7 +389,7 @@ public function testBug3703(): void
389389
18,
390390
],
391391
[
392-
'Property Bug3703\Foo::$bar (array<string, array<string, array<int>>>) does not accept array<string, array<string, array<int>>|string>.',
392+
'Property Bug3703\Foo::$bar (array<string, array<string, array<int>>>) does not accept array<string, array<string, array<int|string>|int>|string>.',
393393
21,
394394
],
395395
]);
@@ -488,7 +488,7 @@ public function testBug6356b(): void
488488
$this->checkExplicitMixed = true;
489489
$this->analyse([__DIR__ . '/data/bug-6356b.php'], [
490490
[
491-
'Property Bug6356b\HelloWorld2::$details (array{name: string, age: int}) does not accept array{name: string, age: \'Forty-two\'}.',
491+
'Property Bug6356b\HelloWorld2::$details (array{name: string, age: int}) does not accept array{name: \'Douglas Adams\', age: \'Forty-two\'}.',
492492
19,
493493
"Offset 'age' (int) does not accept type string.",
494494
],
@@ -498,7 +498,7 @@ public function testBug6356b(): void
498498
"Offset 'age' (int) does not accept type int|string.",
499499
],
500500
[
501-
'Property Bug6356b\HelloWorld2::$nestedDetails (array<array{name: string, age: int}>) does not accept non-empty-array<array{name: string, age: \'Twelve\'|int}>.',
501+
'Property Bug6356b\HelloWorld2::$nestedDetails (array<array{name: string, age: int}>) does not accept non-empty-array<array{name: string, age: \'Eleventy-one\'|\'Twelve\'|int}>.',
502502
26,
503503
"Offset 'age' (int) does not accept type int|string.",
504504
],
@@ -688,6 +688,24 @@ public function testBug12131(): void
688688
]);
689689
}
690690

691+
public function testBug6398(): void
692+
{
693+
$this->checkExplicitMixed = true;
694+
$this->analyse([__DIR__ . '/data/bug-6398.php'], []);
695+
}
696+
697+
public function testBug6571(): void
698+
{
699+
$this->checkExplicitMixed = true;
700+
$this->analyse([__DIR__ . '/data/bug-6571.php'], []);
701+
}
702+
703+
public function testBug7880(): void
704+
{
705+
$this->checkExplicitMixed = true;
706+
$this->analyse([__DIR__ . '/data/bug-7880.php'], []);
707+
}
708+
691709
public function testShortBodySetHook(): void
692710
{
693711
if (PHP_VERSION_ID < 80400) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace Bug6398;
4+
5+
class AsyncTask{
6+
/**
7+
* @phpstan-var \ArrayObject<int, array<string, mixed>>|null
8+
*/
9+
private static $threadLocalStorage = null;
10+
11+
/**
12+
* @param mixed $complexData the data to store
13+
*/
14+
protected function storeLocal(string $key, $complexData) : void{
15+
if(self::$threadLocalStorage === null){
16+
self::$threadLocalStorage = new \ArrayObject();
17+
}
18+
self::$threadLocalStorage[spl_object_id($this)][$key] = $complexData;
19+
}
20+
21+
/**
22+
* @return mixed
23+
*/
24+
protected function fetchLocal(string $key){
25+
$id = spl_object_id($this);
26+
if(self::$threadLocalStorage === null or !isset(self::$threadLocalStorage[$id][$key])){
27+
throw new \InvalidArgumentException("No matching thread-local data found on this thread");
28+
}
29+
30+
return self::$threadLocalStorage[$id][$key];
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1); // lint >= 7.4
2+
3+
namespace Bug6571;
4+
5+
interface ClassLoader{}
6+
7+
class HelloWorld
8+
{
9+
/** @var \Threaded|\ClassLoader[]|null */
10+
private ?\Threaded $classLoaders = null;
11+
12+
/**
13+
* @param \ClassLoader[] $autoloaders
14+
*/
15+
public function setClassLoaders(?array $autoloaders = null) : void{
16+
if($autoloaders === null){
17+
$autoloaders = [];
18+
}
19+
20+
if($this->classLoaders === null){
21+
$this->classLoaders = new \Threaded();
22+
}else{
23+
foreach($this->classLoaders as $k => $autoloader){
24+
unset($this->classLoaders[$k]);
25+
}
26+
}
27+
foreach($autoloaders as $autoloader){
28+
$this->classLoaders[] = $autoloader;
29+
}
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug7880;
4+
5+
class C {
6+
/** @var array{a: string, b: string}|null */
7+
private $a = null;
8+
9+
public function foo(): void {
10+
if ($this->a !== null) {
11+
$this->a['b'] = "baz";
12+
}
13+
}
14+
}

0 commit comments

Comments
 (0)