Skip to content

Commit f40fa5f

Browse files
committed
Implement ArrayAccess->offsetExists narrowing
1 parent 06d592d commit f40fa5f

File tree

4 files changed

+190
-4
lines changed

4 files changed

+190
-4
lines changed

src/Type/ObjectType.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,14 +1149,14 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic
11491149

11501150
public function getOffsetValueType(Type $offsetType): Type
11511151
{
1152-
if (!$this->isExtraOffsetAccessibleClass()->no()) {
1153-
return new MixedType();
1154-
}
1155-
11561152
if ($this->isInstanceOf(ArrayAccess::class)->yes()) {
11571153
return RecursionGuard::run($this, fn (): Type => $this->getMethod('offsetGet', new OutOfClassScope())->getOnlyVariant()->getReturnType());
11581154
}
11591155

1156+
if (!$this->isExtraOffsetAccessibleClass()->no()) {
1157+
return new MixedType();
1158+
}
1159+
11601160
return new ErrorType();
11611161
}
11621162

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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\Reflection\MethodReflection;
13+
use PHPStan\Reflection\ParametersAcceptorSelector;
14+
use PHPStan\Reflection\ReflectionProvider;
15+
use PHPStan\Type\Accessory\HasOffsetType;
16+
use PHPStan\Type\Accessory\HasOffsetValueType;
17+
use PHPStan\Type\Constant\ConstantIntegerType;
18+
use PHPStan\Type\Constant\ConstantStringType;
19+
use PHPStan\Type\Generic\GenericObjectType;
20+
use PHPStan\Type\MethodTypeSpecifyingExtension;
21+
use PHPStan\Type\TypeCombinator;
22+
use function count;
23+
24+
final class ArrayAccessOffsetExistsMethodTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
25+
{
26+
27+
private TypeSpecifier $typeSpecifier;
28+
29+
public function __construct(
30+
private ReflectionProvider $reflectionProvider,
31+
)
32+
{
33+
}
34+
35+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
36+
{
37+
$this->typeSpecifier = $typeSpecifier;
38+
}
39+
40+
public function getClass(): string
41+
{
42+
return ArrayAccess::class;
43+
}
44+
45+
public function isMethodSupported(
46+
MethodReflection $methodReflection,
47+
MethodCall $node,
48+
TypeSpecifierContext $context,
49+
): bool
50+
{
51+
return $methodReflection->getName() === 'offsetExists' && $context->true();
52+
}
53+
54+
public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
55+
{
56+
if (count($node->getArgs()) < 1) {
57+
return new SpecifiedTypes();
58+
}
59+
$key = $node->getArgs()[0]->value;
60+
$keyType = $scope->getType($key);
61+
62+
if (
63+
!$keyType instanceof ConstantStringType
64+
&& !$keyType instanceof ConstantIntegerType
65+
) {
66+
return new SpecifiedTypes();
67+
}
68+
69+
foreach($scope->getType($node->var)->getObjectClassReflections() as $classReflection) {
70+
$implementsTags = $classReflection->getImplementsTags();
71+
72+
if (
73+
!isset($implementsTags[\ArrayAccess::class])
74+
|| !$implementsTags[\ArrayAccess::class]->getType() instanceof GenericObjectType
75+
) {
76+
continue;
77+
}
78+
79+
$implementsType = $implementsTags[\ArrayAccess::class]->getType();
80+
$arrayAccessGenericTypes = $implementsType->getTypes();
81+
if (!isset($arrayAccessGenericTypes[1])) {
82+
continue;
83+
}
84+
85+
return $this->typeSpecifier->create(
86+
$node->var,
87+
new HasOffsetValueType($keyType, $arrayAccessGenericTypes[1]),
88+
$context,
89+
$scope,
90+
);
91+
}
92+
93+
return new SpecifiedTypes();
94+
}
95+
96+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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\Reflection\MethodReflection;
13+
use PHPStan\Type\Accessory\HasOffsetType;
14+
use PHPStan\Type\Constant\ConstantIntegerType;
15+
use PHPStan\Type\Constant\ConstantStringType;
16+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
17+
use PHPStan\Type\MethodTypeSpecifyingExtension;
18+
use PHPStan\Type\Type;
19+
use PHPStan\Type\TypeCombinator;
20+
use function count;
21+
22+
final class ArrayAccessOffsetGetMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
23+
{
24+
public function getClass(): string
25+
{
26+
return ArrayAccess::class;
27+
}
28+
29+
public function isMethodSupported(
30+
MethodReflection $methodReflection,
31+
): bool
32+
{
33+
return $methodReflection->getName() === 'offsetGet';
34+
}
35+
36+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
37+
{
38+
if (count($methodCall->getArgs()) < 1) {
39+
return null;
40+
}
41+
$key = $methodCall->getArgs()[0]->value;
42+
$keyType = $scope->getType($key);
43+
$objectType = $scope->getType($methodCall->var);
44+
45+
if (!$objectType->hasOffsetValueType($keyType)->yes()) {
46+
return null;
47+
}
48+
49+
return $objectType->getOffsetValueType($keyType);
50+
}
51+
52+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 string $vars = '';
14+
}
15+
16+
function doFoo() {
17+
$formView = new FormView();
18+
assertType('Bug3323\FormView', $formView);
19+
if ($formView->offsetExists('_token')) {
20+
assertType("Bug3323\FormView&hasOffsetValue('_token', FormView)", $formView);
21+
22+
$a = $formView->offsetGet('_token');
23+
assertType("Bug3323\FormView", $a);
24+
25+
$a = $formView->offsetGet(123);
26+
assertType("Bug3323\FormView|null", $a);
27+
} else {
28+
assertType('Bug3323\FormView', $formView);
29+
}
30+
assertType('Bug3323\FormView', $formView);
31+
32+
$a = $formView->offsetGet('_token');
33+
assertType("Bug3323\FormView|null", $a);
34+
35+
$a = $formView->offsetGet(123);
36+
assertType("Bug3323\FormView|null", $a);
37+
}
38+

0 commit comments

Comments
 (0)