diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 47101ea9fe..3ebca2524d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1509,6 +1509,18 @@ parameters: count: 1 path: src/Type/PHPStan/ClassNameUsageLocationCreateIdentifierDynamicReturnTypeExtension.php + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/ArrayAccessOffsetExistsMethodTypeSpecifyingExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/ArrayAccessOffsetExistsMethodTypeSpecifyingExtension.php + - message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' identifier: phpstanApi.instanceofType diff --git a/src/Type/Php/ArrayAccessOffsetExistsMethodTypeSpecifyingExtension.php b/src/Type/Php/ArrayAccessOffsetExistsMethodTypeSpecifyingExtension.php new file mode 100644 index 0000000000..c93ca47983 --- /dev/null +++ b/src/Type/Php/ArrayAccessOffsetExistsMethodTypeSpecifyingExtension.php @@ -0,0 +1,88 @@ +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..1ff4521d38 --- /dev/null +++ b/src/Type/Php/ArrayAccessOffsetGetMethodReturnTypeExtension.php @@ -0,0 +1,46 @@ +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..35bf9f1ae9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3323.php @@ -0,0 +1,54 @@ + + */ +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); + + $a = $formView->offsetGet('_token'); + assertType("Bug3323\FormView|null", $a); // could be "null" only + } + assertType('Bug3323\FormView', $formView); + + $a = $formView->offsetGet('_token'); + assertType("Bug3323\FormView|null", $a); + + $a = $formView->offsetGet(123); + assertType("Bug3323\FormView|null", $a); +} +