diff --git a/conf/config.neon b/conf/config.neon index 24e08980f3..28d944928c 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -4,6 +4,7 @@ parameters: bootstrap: null bootstrapFiles: - ../stubs/runtime/ReflectionUnionType.php + - ../stubs/runtime/ReflectionAttribute.php - ../stubs/runtime/Attribute.php excludes_analyse: [] excludePaths: null @@ -1492,6 +1493,11 @@ services: - phpstan.broker.dynamicMethodReturnTypeExtension - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - + class: PHPStan\Type\Php\ReflectionClassGetAttributesMethodReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + exceptionTypeResolver: class: PHPStan\Rules\Exceptions\ExceptionTypeResolver factory: @PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver diff --git a/conf/config.stubFiles.neon b/conf/config.stubFiles.neon index ecdd61bf77..057c2e4a59 100644 --- a/conf/config.stubFiles.neon +++ b/conf/config.stubFiles.neon @@ -1,5 +1,6 @@ parameters: stubFiles: + - ../stubs/ReflectionAttribute.stub - ../stubs/ReflectionClass.stub - ../stubs/iterable.stub - ../stubs/ArrayObject.stub diff --git a/src/Type/Php/ReflectionClassGetAttributesMethodReturnTypeExtension.php b/src/Type/Php/ReflectionClassGetAttributesMethodReturnTypeExtension.php new file mode 100644 index 0000000000..9ef4dd3f6e --- /dev/null +++ b/src/Type/Php/ReflectionClassGetAttributesMethodReturnTypeExtension.php @@ -0,0 +1,58 @@ +getName() === 'getAttributes'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + if (count($methodCall->args) === 0) { + return $this->getDefaultReturnType($scope, $methodCall, $methodReflection); + } + $argType = $scope->getType($methodCall->args[0]->value); + + if ($argType instanceof ConstantStringType) { + $classType = new ObjectType($argType->getValue()); + } elseif ($argType instanceof GenericClassStringType) { + $classType = $argType->getGenericType(); + } else { + return $this->getDefaultReturnType($scope, $methodCall, $methodReflection); + } + + return new ArrayType(new MixedType(), new GenericObjectType(\ReflectionAttribute::class, [$classType])); + } + + private function getDefaultReturnType(Scope $scope, MethodCall $methodCall, MethodReflection $methodReflection): Type + { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->args, + $methodReflection->getVariants() + )->getReturnType(); + } + +} diff --git a/stubs/ReflectionAttribute.stub b/stubs/ReflectionAttribute.stub new file mode 100644 index 0000000000..0bac59de5b --- /dev/null +++ b/stubs/ReflectionAttribute.stub @@ -0,0 +1,21 @@ + + */ + public function getName() : string + { + } + + /** + * @return T + */ + public function newInstance() : object + { + } +} diff --git a/stubs/ReflectionClass.stub b/stubs/ReflectionClass.stub index 09c428b0dc..e5d2a0908a 100644 --- a/stubs/ReflectionClass.stub +++ b/stubs/ReflectionClass.stub @@ -42,4 +42,10 @@ class ReflectionClass */ public function newInstanceWithoutConstructor(); + /** + * @return array> + */ + public function getAttributes(?string $name = null, int $flags = 0) + { + } } diff --git a/stubs/runtime/ReflectionAttribute.php b/stubs/runtime/ReflectionAttribute.php new file mode 100644 index 0000000000..8f0dd0705b --- /dev/null +++ b/stubs/runtime/ReflectionAttribute.php @@ -0,0 +1,38 @@ +gatherAssertTypes(__DIR__ . '/data/bug-3379.php'); } + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/reflectionclass-issue-5511-php8.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/modulo-operator.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/literal-string.php'); diff --git a/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php b/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php new file mode 100644 index 0000000000..ac12dda38c --- /dev/null +++ b/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php @@ -0,0 +1,51 @@ + $genericClassName + */ +function testGetAttributes(string $str, string $className, string $genericClassName): void +{ + $class = new \ReflectionClass(X::class); + + $attrsAll = $class->getAttributes(); + $attrsAbc1 = $class->getAttributes(Abc::class); + $attrsAbc2 = $class->getAttributes(Abc::class, \ReflectionAttribute::IS_INSTANCEOF); + $attrsGCN = $class->getAttributes($genericClassName); + $attrsCN = $class->getAttributes($className); + $attrsStr = $class->getAttributes($str); + $attrsNonsense = $class->getAttributes("some random string"); + + assertType('array>', $attrsAll); + assertType('array>', $attrsAbc1); + assertType('array>', $attrsAbc2); + assertType('array>', $attrsGCN); + assertType('array>', $attrsCN); + assertType('array>', $attrsStr); + assertType('array>', $attrsNonsense); +} + +/** + * @param \ReflectionAttribute $ra + */ +function testNewInstance(\ReflectionAttribute $ra): void +{ + assertType('ReflectionAttribute', $ra); + $abc = $ra->newInstance(); + assertType(Abc::class, $abc); +} diff --git a/tests/PHPStan/Command/CommandHelperTest.php b/tests/PHPStan/Command/CommandHelperTest.php index ff2a6fff2e..6ce191d7ce 100644 --- a/tests/PHPStan/Command/CommandHelperTest.php +++ b/tests/PHPStan/Command/CommandHelperTest.php @@ -169,6 +169,7 @@ public function dataParameters(): array 'bootstrap' => __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'here.php', 'bootstrapFiles' => [ realpath(__DIR__ . '/../../../stubs/runtime/ReflectionUnionType.php'), + realpath(__DIR__ . '/../../../stubs/runtime/ReflectionAttribute.php'), realpath(__DIR__ . '/../../../stubs/runtime/Attribute.php'), __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'here.php', ],