From 065fad5dd716558d954075bbc32093cec55594e2 Mon Sep 17 00:00:00 2001 From: Jochem Klaver Date: Fri, 13 May 2022 16:53:49 +0200 Subject: [PATCH] Catch and process errors that occure during annotation instantiation When an annotation class is instantiated and values are passed to the constructor, TypeErrors and ArgumentCountErrors can be thrown. This change catches those exceptions and shows an AnnotationException containing the annotation name and context. --- .../Annotations/AnnotationException.php | 5 +- lib/Doctrine/Common/Annotations/DocParser.php | 36 ++++++- phpstan.neon | 1 + .../Common/Annotations/DocParserTest.php | 100 +++++++++++++++--- 4 files changed, 122 insertions(+), 20 deletions(-) diff --git a/lib/Doctrine/Common/Annotations/AnnotationException.php b/lib/Doctrine/Common/Annotations/AnnotationException.php index b1ea64e6f..4d91825e5 100644 --- a/lib/Doctrine/Common/Annotations/AnnotationException.php +++ b/lib/Doctrine/Common/Annotations/AnnotationException.php @@ -3,6 +3,7 @@ namespace Doctrine\Common\Annotations; use Exception; +use Throwable; use function get_class; use function gettype; @@ -47,9 +48,9 @@ public static function semanticalError($message) * * @return AnnotationException */ - public static function creationError($message) + public static function creationError($message, ?Throwable $previous = null) { - return new self('[Creation Error] ' . $message); + return new self('[Creation Error] ' . $message, 0, $previous); } /** diff --git a/lib/Doctrine/Common/Annotations/DocParser.php b/lib/Doctrine/Common/Annotations/DocParser.php index ae530c50f..80f307cab 100644 --- a/lib/Doctrine/Common/Annotations/DocParser.php +++ b/lib/Doctrine/Common/Annotations/DocParser.php @@ -12,6 +12,7 @@ use ReflectionProperty; use RuntimeException; use stdClass; +use Throwable; use function array_keys; use function array_map; @@ -941,7 +942,7 @@ private function Annotation() if (self::$annotationMetadata[$name]['has_named_argument_constructor']) { if (PHP_VERSION_ID >= 80000) { - return new $name(...$values); + return $this->instantiateAnnotiation($originalName, $this->context, $name, $values); } $positionalValues = []; @@ -968,16 +969,16 @@ private function Annotation() $positionalValues[self::$annotationMetadata[$name]['constructor_args'][$property]['position']] = $value; } - return new $name(...$positionalValues); + return $this->instantiateAnnotiation($originalName, $this->context, $name, $positionalValues); } // check if the annotation expects values via the constructor, // or directly injected into public properties if (self::$annotationMetadata[$name]['has_constructor'] === true) { - return new $name($values); + return $this->instantiateAnnotiation($originalName, $this->context, $name, [$values]); } - $instance = new $name(); + $instance = $this->instantiateAnnotiation($originalName, $this->context, $name, []); foreach ($values as $property => $value) { if (! isset(self::$annotationMetadata[$name]['properties'][$property])) { @@ -1456,4 +1457,31 @@ private function resolvePositionalValues(array $arguments, string $name): array return $values; } + + /** + * Try to instantiate the annotation and catch and process any exceptions related to failure + * + * @param class-string $name + * @param array $arguments + * + * @return object + * + * @throws AnnotationException + */ + private function instantiateAnnotiation(string $originalName, string $context, string $name, array $arguments) + { + try { + return new $name(...$arguments); + } catch (Throwable $exception) { + throw AnnotationException::creationError( + sprintf( + 'An error occurred while instantiating the annotation @%s declared on %s: "%s".', + $originalName, + $context, + $exception->getMessage() + ), + $exception + ); + } + } } diff --git a/phpstan.neon b/phpstan.neon index ad8f23951..78663a129 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -13,6 +13,7 @@ parameters: ignoreErrors: - '#Instantiated class Doctrine_Tests_Common_Annotations_Fixtures_ClassNoNamespaceNoComment not found#' - '#Property Doctrine\\Tests\\Common\\Annotations\\DummyClassNonAnnotationProblem::\$foo has unknown class#' + - '#Call to an undefined static method PHPUnit\\Framework\\TestCase::expectExceptionMessageRegExp\(\)#' # That tag is empty on purpose - '#PHPDoc tag @var has invalid value \(\)\: Unexpected token "\*/", expected type at offset 9#' diff --git a/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php b/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php index fad9bf93b..5e6f35cb2 100644 --- a/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php +++ b/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php @@ -14,15 +14,19 @@ use Doctrine\Tests\Common\Annotations\Fixtures\ClassWithConstants; use Doctrine\Tests\Common\Annotations\Fixtures\InterfaceWithConstants; use InvalidArgumentException; +use PHPUnit\Framework\Constraint\ExceptionMessage; use PHPUnit\Framework\TestCase; use ReflectionClass; +use TypeError; use function array_column; use function array_combine; use function assert; use function class_exists; use function extension_loaded; +use function get_parent_class; use function ini_get; +use function method_exists; use function sprintf; use function ucfirst; @@ -886,9 +890,16 @@ public function testAnnotationEnumInvalidTypeDeclarationException(): void $docblock = '@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationEnumInvalid("foo")'; $parser->setIgnoreNotImportedAnnotations(false); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('@Enum supports only scalar values "array" given.'); - $parser->parse($docblock); + $this->expectException(AnnotationException::class); + try { + $parser->parse($docblock); + } catch (AnnotationException $exc) { + $previous = $exc->getPrevious(); + $this->assertInstanceOf(InvalidArgumentException::class, $previous); + $this->assertThat($previous, new ExceptionMessage('@Enum supports only scalar values "array" given.')); + + throw $exc; + } } public function testAnnotationEnumInvalidLiteralDeclarationException(): void @@ -897,9 +908,19 @@ public function testAnnotationEnumInvalidLiteralDeclarationException(): void $docblock = '@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationEnumLiteralInvalid("foo")'; $parser->setIgnoreNotImportedAnnotations(false); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Undefined enumerator value "3" for literal "AnnotationEnumLiteral::THREE".'); - $parser->parse($docblock); + $this->expectException(AnnotationException::class); + try { + $parser->parse($docblock); + } catch (AnnotationException $exc) { + $previous = $exc->getPrevious(); + $this->assertInstanceOf(InvalidArgumentException::class, $previous); + $this->assertThat( + $previous, + new ExceptionMessage('Undefined enumerator value "3" for literal "AnnotationEnumLiteral::THREE".') + ); + + throw $exc; + } } /** @@ -1100,11 +1121,21 @@ public function testAnnotationWithInvalidTargetDeclarationError(): void DOCBLOCK; $parser->setTarget(Target::TARGET_CLASS); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage( - 'Invalid Target "Foo". Available targets: [ALL, CLASS, METHOD, PROPERTY, FUNCTION, ANNOTATION]' - ); - $parser->parse($docblock, $context); + $this->expectException(AnnotationException::class); + try { + $parser->parse($docblock, $context); + } catch (AnnotationException $exc) { + $previous = $exc->getPrevious(); + $this->assertInstanceOf(InvalidArgumentException::class, $previous); + $this->assertThat( + $previous, + new ExceptionMessage( + 'Invalid Target "Foo". Available targets: [ALL, CLASS, METHOD, PROPERTY, FUNCTION, ANNOTATION]' + ) + ); + + throw $exc; + } } public function testAnnotationWithTargetEmptyError(): void @@ -1118,9 +1149,19 @@ public function testAnnotationWithTargetEmptyError(): void DOCBLOCK; $parser->setTarget(Target::TARGET_CLASS); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('@Target expects either a string value, or an array of strings, "NULL" given.'); - $parser->parse($docblock, $context); + $this->expectException(AnnotationException::class); + try { + $parser->parse($docblock, $context); + } catch (AnnotationException $exc) { + $previous = $exc->getPrevious(); + $this->assertInstanceOf(InvalidArgumentException::class, $previous); + $this->assertThat( + $previous, + new ExceptionMessage('@Target expects either a string value, or an array of strings, "NULL" given.') + ); + + throw $exc; + } } /** @@ -1683,6 +1724,37 @@ public function testNamedArgumentsConstructorAnnotationWithInvalidArguments(): v ); $parser->parse('/** @AnotherNamedAnnotation("foo", bar=666, "hey") */'); } + + public function testNamedArgumentsConstructorAnnotationWithWrongArgumentType(): void + { + $context = 'property SomeClassName::invalidProperty.'; + $docblock = '@NamedAnnotationWithArray(foo = "no array!")'; + $parser = $this->createTestParser(); + $this->expectException(AnnotationException::class); + $this->expectExceptionMessageMatches( + '/\[Creation Error\] An error occurred while instantiating the annotation ' + . '@NamedAnnotationWithArray declared on property SomeClassName::invalidProperty\.: ".*"\.$/' + ); + try { + $parser->parse($docblock, $context); + } catch (AnnotationException $exc) { + $this->assertInstanceOf(TypeError::class, $exc->getPrevious()); + + throw $exc; + } + } + + /** + * Override for BC with PHPUnit <8 + */ + public function expectExceptionMessageMatches(string $regularExpression): void + { + if (method_exists(get_parent_class($this), 'expectExceptionMessageMatches')) { + parent::expectExceptionMessageMatches($regularExpression); + } else { + parent::expectExceptionMessageRegExp($regularExpression); + } + } } /** @Annotation */