From f40fa5f355b683d43016c215ab0939f1caf333f5 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 --- src/Type/ObjectType.php | 8 +- ...setExistsMethodTypeSpecifyingExtension.php | 96 +++++++++++++++++++ ...cessOffsetGetMethodReturnTypeExtension.php | 52 ++++++++++ tests/PHPStan/Analyser/nsrt/bug-3323.php | 38 ++++++++ 4 files changed, 190 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/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..0bafc3d713 --- /dev/null +++ b/src/Type/Php/ArrayAccessOffsetExistsMethodTypeSpecifyingExtension.php @@ -0,0 +1,96 @@ +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..0ebb0731b3 --- /dev/null +++ b/src/Type/Php/ArrayAccessOffsetGetMethodReturnTypeExtension.php @@ -0,0 +1,52 @@ +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..d99961f195 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3323.php @@ -0,0 +1,38 @@ + + */ +class FormView implements \ArrayAccess +{ + public string $vars = ''; +} + +function doFoo() { + $formView = new FormView(); + assertType('Bug3323\FormView', $formView); + if ($formView->offsetExists('_token')) { + assertType("Bug3323\FormView&hasOffsetValue('_token', 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); +} +