Skip to content

Commit a1a81b8

Browse files
committed
Implement ArrayAccess->offsetExists narrowing
1 parent 252390f commit a1a81b8

File tree

4 files changed

+200
-0
lines changed

4 files changed

+200
-0
lines changed

phpstan-baseline.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,18 @@ parameters:
15091509
count: 1
15101510
path: src/Type/PHPStan/ClassNameUsageLocationCreateIdentifierDynamicReturnTypeExtension.php
15111511

1512+
-
1513+
message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#'
1514+
identifier: phpstanApi.instanceofType
1515+
count: 1
1516+
path: src/Type/Php/ArrayAccessOffsetExistsMethodTypeSpecifyingExtension.php
1517+
1518+
-
1519+
message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#'
1520+
identifier: phpstanApi.instanceofType
1521+
count: 1
1522+
path: src/Type/Php/ArrayAccessOffsetExistsMethodTypeSpecifyingExtension.php
1523+
15121524
-
15131525
message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#'
15141526
identifier: phpstanApi.instanceofType
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use ArrayAccess;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Analyser\SpecifiedTypes;
9+
use PHPStan\Analyser\TypeSpecifier;
10+
use PHPStan\Analyser\TypeSpecifierAwareExtension;
11+
use PHPStan\Analyser\TypeSpecifierContext;
12+
use PHPStan\DependencyInjection\AutowiredService;
13+
use PHPStan\Reflection\MethodReflection;
14+
use PHPStan\Type\Accessory\HasOffsetValueType;
15+
use PHPStan\Type\Constant\ConstantIntegerType;
16+
use PHPStan\Type\Constant\ConstantStringType;
17+
use PHPStan\Type\Generic\GenericObjectType;
18+
use PHPStan\Type\MethodTypeSpecifyingExtension;
19+
use function count;
20+
21+
#[AutowiredService]
22+
final class ArrayAccessOffsetExistsMethodTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
23+
{
24+
25+
private TypeSpecifier $typeSpecifier;
26+
27+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
28+
{
29+
$this->typeSpecifier = $typeSpecifier;
30+
}
31+
32+
public function getClass(): string
33+
{
34+
return ArrayAccess::class;
35+
}
36+
37+
public function isMethodSupported(
38+
MethodReflection $methodReflection,
39+
MethodCall $node,
40+
TypeSpecifierContext $context,
41+
): bool
42+
{
43+
return $methodReflection->getName() === 'offsetExists' && $context->true();
44+
}
45+
46+
public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
47+
{
48+
if (count($node->getArgs()) < 1) {
49+
return new SpecifiedTypes();
50+
}
51+
$key = $node->getArgs()[0]->value;
52+
$keyType = $scope->getType($key);
53+
54+
if (
55+
!$keyType instanceof ConstantStringType
56+
&& !$keyType instanceof ConstantIntegerType
57+
) {
58+
return new SpecifiedTypes();
59+
}
60+
61+
foreach ($scope->getType($node->var)->getObjectClassReflections() as $classReflection) {
62+
$implementsTags = $classReflection->getImplementsTags();
63+
64+
if (
65+
!isset($implementsTags[ArrayAccess::class])
66+
|| !$implementsTags[ArrayAccess::class]->getType() instanceof GenericObjectType
67+
) {
68+
continue;
69+
}
70+
71+
$implementsType = $implementsTags[ArrayAccess::class]->getType();
72+
$arrayAccessGenericTypes = $implementsType->getTypes();
73+
if (!isset($arrayAccessGenericTypes[1])) {
74+
continue;
75+
}
76+
77+
return $this->typeSpecifier->create(
78+
$node->var,
79+
new HasOffsetValueType($keyType, $arrayAccessGenericTypes[1]),
80+
$context,
81+
$scope,
82+
);
83+
}
84+
85+
return new SpecifiedTypes();
86+
}
87+
88+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use ArrayAccess;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\DependencyInjection\AutowiredService;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
11+
use PHPStan\Type\Type;
12+
use function count;
13+
14+
#[AutowiredService]
15+
final class ArrayAccessOffsetGetMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
16+
{
17+
18+
public function getClass(): string
19+
{
20+
return ArrayAccess::class;
21+
}
22+
23+
public function isMethodSupported(
24+
MethodReflection $methodReflection,
25+
): bool
26+
{
27+
return $methodReflection->getName() === 'offsetGet';
28+
}
29+
30+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
31+
{
32+
if (count($methodCall->getArgs()) < 1) {
33+
return null;
34+
}
35+
$key = $methodCall->getArgs()[0]->value;
36+
$keyType = $scope->getType($key);
37+
$objectType = $scope->getType($methodCall->var);
38+
39+
if (!$objectType->hasOffsetValueType($keyType)->yes()) {
40+
return null;
41+
}
42+
43+
return $objectType->getOffsetValueType($keyType);
44+
}
45+
46+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace Bug3323;
4+
5+
use function PHPStan\Testing\assertType;
6+
use ArrayAccess;
7+
8+
/**
9+
* @implements ArrayAccess<string, self>
10+
*/
11+
class FormView implements \ArrayAccess
12+
{
13+
public array $vars = [];
14+
15+
public function offsetExists($offset) {
16+
return array_key_exists($offset, $this->vars);
17+
}
18+
public function offsetGet($offset) {
19+
return $this->vars[$offset] ?? null;
20+
}
21+
public function offsetSet($offset, $value) {
22+
$this->vars[$offset] = $value;
23+
}
24+
public function offsetUnset($offset) {
25+
unset($this->vars[$offset]);
26+
}
27+
}
28+
29+
function doFoo() {
30+
$formView = new FormView();
31+
assertType('Bug3323\FormView', $formView);
32+
if ($formView->offsetExists('_token')) {
33+
assertType("Bug3323\FormView&hasOffsetValue('_token', Bug3323\FormView)", $formView);
34+
35+
$a = $formView->offsetGet('_token');
36+
assertType("Bug3323\FormView", $a);
37+
38+
$a = $formView->offsetGet(123);
39+
assertType("Bug3323\FormView|null", $a);
40+
} else {
41+
assertType('Bug3323\FormView', $formView);
42+
43+
$a = $formView->offsetGet('_token');
44+
assertType("Bug3323\FormView|null", $a); // could be "null" only
45+
}
46+
assertType('Bug3323\FormView', $formView);
47+
48+
$a = $formView->offsetGet('_token');
49+
assertType("Bug3323\FormView|null", $a);
50+
51+
$a = $formView->offsetGet(123);
52+
assertType("Bug3323\FormView|null", $a);
53+
}
54+

0 commit comments

Comments
 (0)