From 4482676732fc7ad485866075033d9596377493f5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 29 Dec 2024 14:23:28 +0100 Subject: [PATCH] Implement `ArrayAccess->offsetExists` narrowing --- conf/config.neon | 10 +++ src/Type/ObjectType.php | 8 +- ...setExistsMethodTypeSpecifyingExtension.php | 87 +++++++++++++++++++ ...cessOffsetGetMethodReturnTypeExtension.php | 44 ++++++++++ tests/PHPStan/Analyser/nsrt/bug-3323.php | 51 +++++++++++ 5 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 src/Type/Php/ArrayAccessOffsetExistsMethodTypeSpecifyingExtension.php create mode 100644 src/Type/Php/ArrayAccessOffsetGetMethodReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-3323.php diff --git a/conf/config.neon b/conf/config.neon index 4d7c3b4e99..22e9dbb8bc 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1097,6 +1097,16 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArrayAccessOffsetExistsMethodTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension + + - + class: PHPStan\Type\Php\ArrayAccessOffsetGetMethodReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - class: PHPStan\Type\Php\ArrayIntersectKeyFunctionReturnTypeExtension tags: diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 9294fa2078..1d24519719 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -1149,14 +1149,14 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { - if (!$this->isExtraOffsetAccessibleClass()->no()) { - return new MixedType(); - } - if ($this->isInstanceOf(ArrayAccess::class)->yes()) { return RecursionGuard::run($this, fn (): Type => $this->getMethod('offsetGet', new OutOfClassScope())->getOnlyVariant()->getReturnType()); } + if (!$this->isExtraOffsetAccessibleClass()->no()) { + return new MixedType(); + } + return new ErrorType(); } diff --git a/src/Type/Php/ArrayAccessOffsetExistsMethodTypeSpecifyingExtension.php b/src/Type/Php/ArrayAccessOffsetExistsMethodTypeSpecifyingExtension.php new file mode 100644 index 0000000000..74f52a2eea --- /dev/null +++ b/src/Type/Php/ArrayAccessOffsetExistsMethodTypeSpecifyingExtension.php @@ -0,0 +1,87 @@ +typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return ArrayAccess::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context, + ): bool + { + return $methodReflection->getName() === 'offsetExists' && $context->true(); + } + + public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + if (count($node->getArgs()) < 1) { + return new SpecifiedTypes(); + } + $key = $node->getArgs()[0]->value; + $keyType = $scope->getType($key); + + if ( + !$keyType instanceof ConstantStringType + && !$keyType instanceof ConstantIntegerType + ) { + return new SpecifiedTypes(); + } + + foreach ($scope->getType($node->var)->getObjectClassReflections() as $classReflection) { + $implementsTags = $classReflection->getImplementsTags(); + + if ( + !isset($implementsTags[ArrayAccess::class]) + || !$implementsTags[ArrayAccess::class]->getType() instanceof GenericObjectType + ) { + continue; + } + + $implementsType = $implementsTags[ArrayAccess::class]->getType(); + $arrayAccessGenericTypes = $implementsType->getTypes(); + if (!isset($arrayAccessGenericTypes[1])) { + continue; + } + + return $this->typeSpecifier->create( + $node->var, + new HasOffsetValueType($keyType, $arrayAccessGenericTypes[1]), + $context, + $scope, + ); + } + + return new SpecifiedTypes(); + } + +} diff --git a/src/Type/Php/ArrayAccessOffsetGetMethodReturnTypeExtension.php b/src/Type/Php/ArrayAccessOffsetGetMethodReturnTypeExtension.php new file mode 100644 index 0000000000..e63fbfc42a --- /dev/null +++ b/src/Type/Php/ArrayAccessOffsetGetMethodReturnTypeExtension.php @@ -0,0 +1,44 @@ +getName() === 'offsetGet'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) < 1) { + return null; + } + $key = $methodCall->getArgs()[0]->value; + $keyType = $scope->getType($key); + $objectType = $scope->getType($methodCall->var); + + if (!$objectType->hasOffsetValueType($keyType)->yes()) { + return null; + } + + return $objectType->getOffsetValueType($keyType); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3323.php b/tests/PHPStan/Analyser/nsrt/bug-3323.php new file mode 100644 index 0000000000..6e63742527 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3323.php @@ -0,0 +1,51 @@ + + */ +class FormView implements \ArrayAccess +{ + public array $vars = []; + + public function offsetExists($offset) { + return array_key_exists($offset, $this->vars); + } + public function offsetGet($offset) { + return $this->vars[$offset] ?? null; + } + public function offsetSet($offset, $value) { + $this->vars[$offset] = $value; + } + public function offsetUnset($offset) { + unset($this->vars[$offset]); + } +} + +function doFoo() { + $formView = new FormView(); + assertType('Bug3323\FormView', $formView); + if ($formView->offsetExists('_token')) { + assertType("Bug3323\FormView&hasOffsetValue('_token', Bug3323\FormView)", $formView); + + $a = $formView->offsetGet('_token'); + assertType("Bug3323\FormView", $a); + + $a = $formView->offsetGet(123); + assertType("Bug3323\FormView|null", $a); + } else { + assertType('Bug3323\FormView', $formView); + } + assertType('Bug3323\FormView', $formView); + + $a = $formView->offsetGet('_token'); + assertType("Bug3323\FormView|null", $a); + + $a = $formView->offsetGet(123); + assertType("Bug3323\FormView|null", $a); +} +