diff --git a/.doctrine-project.json b/.doctrine-project.json index 38553aa66..d62df10e9 100644 --- a/.doctrine-project.json +++ b/.doctrine-project.json @@ -5,15 +5,15 @@ "docsSlug": "doctrine-annotations", "versions": [ { - "name": "1.11", - "branchName": "master", + "name": "1.14", + "branchName": "1.14.x", "slug": "latest", "upcoming": true }, { - "name": "1.10", - "branchName": "1.10.x", - "slug": "1.10", + "name": "1.13", + "branchName": "1.13.x", + "slug": "1.13", "aliases": [ "current", "stable" @@ -21,6 +21,24 @@ "current": true, "maintained": true }, + { + "name": "1.12", + "branchName": "1.12.x", + "slug": "1.12", + "maintained": false + }, + { + "name": "1.11", + "branchName": "1.11.x", + "slug": "1.11", + "maintained": false + }, + { + "name": "1.10", + "branchName": "1.10.x", + "slug": "1.10", + "maintained": false + }, { "name": "1.9", "branchName": "1.9.x", diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index f710db645..83fd68b5e 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -12,29 +12,4 @@ on: jobs: coding-standards: name: "Coding Standards" - runs-on: "ubuntu-20.04" - - strategy: - matrix: - php-version: - - "7.4" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2" - - - name: "Install PHP" - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - tools: "cs2pr" - - - name: "Install dependencies with Composer" - uses: "ramsey/composer-install@v1" - with: - dependency-versions: "highest" - - # https://github.com/doctrine/.github/issues/3 - - name: "Run PHP_CodeSniffer" - run: "vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr" + uses: "doctrine/.github/.github/workflows/coding-standards.yml@2.1.0" diff --git a/.github/workflows/composer-lint.yml b/.github/workflows/composer-lint.yml new file mode 100644 index 000000000..091b491c3 --- /dev/null +++ b/.github/workflows/composer-lint.yml @@ -0,0 +1,18 @@ +name: "Composer Lint" + +on: + pull_request: + branches: + - "*.x" + paths: + - "composer.json" + push: + branches: + - "*.x" + paths: + - "composer.json" + +jobs: + composer-lint: + name: "Composer Lint" + uses: "doctrine/.github/.github/workflows/composer-lint.yml@2.1.0" diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index e4bdcf4b7..bbd54d485 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -14,77 +14,6 @@ env: jobs: phpunit: name: "PHPUnit" - runs-on: "ubuntu-20.04" - - strategy: - matrix: - php-version: - - "7.1" - - "7.2" - - "7.3" - - "7.4" - - "8.0" - dependencies: - - "highest" - include: - - php-version: "7.2" - dependencies: "lowest" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2" - with: - fetch-depth: 2 - - - name: "Install PHP with PCOV" - uses: "shivammathur/setup-php@v2" - if: "${{ matrix.php-version != '7.1' }}" - with: - php-version: "${{ matrix.php-version }}" - coverage: "pcov" - ini-values: "zend.assertions=1" - - - name: "Install PHP with XDebug" - uses: "shivammathur/setup-php@v2" - if: "${{ matrix.php-version == '7.1' }}" - with: - php-version: "${{ matrix.php-version }}" - coverage: "xdebug" - ini-values: "zend.assertions=1" - - - name: "Install dependencies with Composer" - uses: "ramsey/composer-install@v1" - with: - dependency-versions: "${{ matrix.dependencies }}" - composer-options: "--prefer-dist" - - - name: "Run PHPUnit" - run: "vendor/bin/phpunit --coverage-clover=coverage.xml" - - - name: "Upload coverage file" - uses: "actions/upload-artifact@v2" - with: - name: "phpunit-${{ matrix.php-version }}.coverage" - path: "coverage.xml" - - upload_coverage: - name: "Upload coverage to Codecov" - runs-on: "ubuntu-20.04" - needs: - - "phpunit" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2" - with: - fetch-depth: 2 - - - name: "Download coverage files" - uses: "actions/download-artifact@v2" - with: - path: "reports" - - - name: "Upload to Codecov" - uses: "codecov/codecov-action@v1" - with: - directory: "reports" + uses: "doctrine/.github/.github/workflows/continuous-integration.yml@2.1.0" + with: + php-versions: '["7.2", "7.3", "7.4", "8.0", "8.1", "8.2"]' diff --git a/.github/workflows/release-on-milestone-closed.yml b/.github/workflows/release-on-milestone-closed.yml index 70ccbdb2d..949de8201 100644 --- a/.github/workflows/release-on-milestone-closed.yml +++ b/.github/workflows/release-on-milestone-closed.yml @@ -8,48 +8,9 @@ on: jobs: release: name: "Git tag, release & create merge-up PR" - runs-on: "ubuntu-20.04" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2" - - - name: "Release" - uses: "laminas/automatic-releases@v1" - with: - command-name: "laminas:automatic-releases:release" - env: - "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }} - "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }} - "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }} - "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }} - - - name: "Create Merge-Up Pull Request" - uses: "laminas/automatic-releases@v1" - with: - command-name: "laminas:automatic-releases:create-merge-up-pull-request" - env: - "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }} - "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }} - "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }} - "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }} - - - name: "Create and/or Switch to new Release Branch" - uses: "laminas/automatic-releases@v1" - with: - command-name: "laminas:automatic-releases:switch-default-branch-to-next-minor" - env: - "GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }} - "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }} - "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }} - "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }} - - - name: "Create new milestones" - uses: "laminas/automatic-releases@v1" - with: - command-name: "laminas:automatic-releases:create-milestones" - env: - "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }} - "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }} - "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }} - "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }} + uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@2.1.0" + secrets: + GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} + GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} + ORGANIZATION_ADMIN_TOKEN: ${{ secrets.ORGANIZATION_ADMIN_TOKEN }} + SIGNING_SECRET_KEY: ${{ secrets.SIGNING_SECRET_KEY }} diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index d0e0d857d..0adc3d08e 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -10,29 +10,6 @@ on: - "*.x" jobs: - static-analysis-phpstan: - name: "Static Analysis with PHPStan" - runs-on: "ubuntu-20.04" - - strategy: - matrix: - php-version: - - "7.4" - - steps: - - name: "Checkout code" - uses: "actions/checkout@v2" - - - name: "Install PHP" - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - - - name: "Install dependencies with Composer" - uses: "ramsey/composer-install@v1" - with: - dependency-versions: "highest" - - - name: "Run a static analysis with phpstan/phpstan" - run: "vendor/bin/phpstan analyse" + static-analysis: + name: "Static Analysis" + uses: "doctrine/.github/.github/workflows/static-analysis.yml@2.1.0" diff --git a/README.md b/README.md index c2c7eb7ba..6b8c0359b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +⚠️ PHP 8 introduced +[attributes](https://www.php.net/manual/en/language.attributes.overview.php), +which are a native replacement for annotations. As such, this library is +considered feature complete, and should receive exclusively bugfixes and +security fixes. + # Doctrine Annotations [![Build Status](https://github.com/doctrine/annotations/workflows/Continuous%20Integration/badge.svg?label=build)](https://github.com/doctrine/persistence/actions) diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 000000000..4172708f0 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,18 @@ +# Upgrade from 1.0.x to 2.0.x + +- The `NamedArgumentConstructorAnnotation` has been removed. Use the `@NamedArgumentConstructor` + annotation instead. +- `SimpleAnnotationReader` has been removed. +- `DocLexer::peek()` and `DocLexer::glimpse` now return +`Doctrine\Common\Lexer\Token` objects. When using `doctrine/lexer` 2, these +implement `ArrayAccess` as a way for you to still be able to treat them as +arrays in some ways. +- `CachedReader` and `FileCacheReader` have been removed. +- `AnnotationRegistry` methods related to registering annotations instead of + using autoloading have been removed. +- Parameter type declarations have been added to all methods of all classes. If +you have classes inheriting from classes inside this package, you should add +parameter and return type declarations. +- Support for PHP < 7.2 has been removed +- `PhpParser::parseClass()` has been removed. Use + `PhpParser::parseUseStatements()` instead. diff --git a/composer.json b/composer.json index 00d023107..d1d3d8db1 100644 --- a/composer.json +++ b/composer.json @@ -1,35 +1,57 @@ { "name": "doctrine/annotations", - "type": "library", "description": "Docblock Annotations Parser", - "keywords": ["annotations", "docblock", "parser"], - "homepage": "https://www.doctrine-project.org/projects/annotations.html", "license": "MIT", + "type": "library", + "keywords": [ + "annotations", + "docblock", + "parser" + ], "authors": [ - {"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"}, - {"name": "Roman Borschel", "email": "roman@code-factory.org"}, - {"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"}, - {"name": "Jonathan Wage", "email": "jonwage@gmail.com"}, - {"name": "Johannes Schmitt", "email": "schmittjoh@gmail.com"} + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } ], + "homepage": "https://www.doctrine-project.org/projects/annotations.html", "require": { - "php": "^7.1 || ^8.0", + "php": "^7.2 || ^8.0", "ext-tokenizer": "*", - "doctrine/lexer": "1.*", + "doctrine/lexer": "^2 || ^3", "psr/cache": "^1 || ^2 || ^3" }, "require-dev": { - "doctrine/cache": "^1.11 || ^2.0", - "doctrine/coding-standard": "^6.0 || ^8.1", - "phpstan/phpstan": "^0.12.20", - "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", - "symfony/cache": "^4.4 || ^5.2" + "doctrine/cache": "^2.0", + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.8.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^5.4 || ^6", + "vimeo/psalm": "^4.10" }, - "config": { - "sort-packages": true + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" }, "autoload": { - "psr-4": { "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" } + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } }, "autoload-dev": { "psr-4": { @@ -40,5 +62,11 @@ "tests/Doctrine/Tests/Common/Annotations/Fixtures/functions.php", "tests/Doctrine/Tests/Common/Annotations/Fixtures/SingleClassLOC1000.php" ] + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "sort-packages": true } } diff --git a/docs/en/annotations.rst b/docs/en/annotations.rst index 2c3c42865..d32b15d12 100644 --- a/docs/en/annotations.rst +++ b/docs/en/annotations.rst @@ -79,23 +79,11 @@ with Doctrine Annotations requires this setup: .. code-block:: php use Doctrine\Common\Annotations\AnnotationReader; - use Doctrine\Common\Annotations\AnnotationRegistry; - - AnnotationRegistry::registerFile("/path/to/doctrine/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php"); - AnnotationRegistry::registerAutoloadNamespace("Symfony\Component\Validator\Constraint", "/path/to/symfony/src"); - AnnotationRegistry::registerAutoloadNamespace("MyProject\Annotations", "/path/to/myproject/src"); $reader = new AnnotationReader(); AnnotationReader::addGlobalIgnoredName('dummy'); -The second block with the annotation registry calls registers all the -three different annotation namespaces that are used. -Doctrine Annotations saves all its annotations in a single file, that is -why ``AnnotationRegistry#registerFile`` is used in contrast to -``AnnotationRegistry#registerAutoloadNamespace`` which creates a PSR-0 -compatible loading mechanism for class to file names. - -In the third block, we create the actual ``AnnotationReader`` instance. +We create the actual ``AnnotationReader`` instance. Note that we also add ``dummy`` to the global list of ignored annotations for which we do not throw exceptions. Setting this is necessary in our example case, otherwise ``@dummy`` would trigger an @@ -165,57 +153,6 @@ class name you can wrap the reader in an ``IndexedReader``: indexed or numeric keys, otherwise your code may experience failures due to caching in a numerical or indexed format. -Registering Annotations -~~~~~~~~~~~~~~~~~~~~~~~ - -As explained in the introduction, Doctrine Annotations uses its own -autoloading mechanism to determine if a given annotation has a -corresponding PHP class that can be autoloaded. For annotation -autoloading you have to configure the -``Doctrine\Common\Annotations\AnnotationRegistry``. There are three -different mechanisms to configure annotation autoloading: - -- Calling ``AnnotationRegistry#registerFile($file)`` to register a file - that contains one or more annotation classes. -- Calling ``AnnotationRegistry#registerNamespace($namespace, $dirs = - null)`` to register that the given namespace contains annotations and - that their base directory is located at the given $dirs or in the - include path if ``NULL`` is passed. The given directories should *NOT* - be the directory where classes of the namespace are in, but the base - directory of the root namespace. The AnnotationRegistry uses a - namespace to directory separator approach to resolve the correct path. -- Calling ``AnnotationRegistry#registerLoader($callable)`` to register - an autoloader callback. The callback accepts the class as first and - only parameter and has to return ``true`` if the corresponding file - was found and included. - -.. note:: - - Loaders have to fail silently, if a class is not found even if it - matches for example the namespace prefix of that loader. Never is a - loader to throw a warning or exception if the loading failed - otherwise parsing doc block annotations will become a huge pain. - -A sample loader callback could look like: - -.. code-block:: php - - use Doctrine\Common\Annotations\AnnotationRegistry; - use Symfony\Component\ClassLoader\UniversalClassLoader; - - AnnotationRegistry::registerLoader(function($class) { - $file = str_replace("\\", DIRECTORY_SEPARATOR, $class) . ".php"; - - if (file_exists("/my/base/path/" . $file)) { - // file_exists() makes sure that the loader fails silently - require "/my/base/path/" . $file; - } - }); - - $loader = new UniversalClassLoader(); - AnnotationRegistry::registerLoader(array($loader, "loadClass")); - - Ignoring missing exceptions ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/en/custom.rst b/docs/en/custom.rst index 11fbe1a31..300516625 100644 --- a/docs/en/custom.rst +++ b/docs/en/custom.rst @@ -2,7 +2,7 @@ Custom Annotation Classes ========================= If you want to define your own annotations, you just have to group them -in a namespace and register this namespace in the ``AnnotationRegistry``. +in a namespace. Annotation classes have to contain a class-level docblock with the text ``@Annotation``: @@ -58,10 +58,10 @@ Optional: Constructors with Named Parameters Starting with Annotations v1.11 a new annotation instantiation strategy is available that aims at compatibility of Annotation classes with the PHP 8 -attribute feature. You need to declare a constructor with regular parameter +attribute feature. You need to declare a constructor with regular parameter names that match the named arguments in the annotation syntax. -To enable this feature, you can tag your annotation class with +To enable this feature, you can tag your annotation class with ``@NamedArgumentConstructor`` (available from v1.12) or implement the ``Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation`` interface (available from v1.11 and deprecated as of v1.12). @@ -69,14 +69,14 @@ When using the ``@NamedArgumentConstructor`` tag, the first argument of the constructor is considered as the default one. -Usage with the ``@NamedArgumentContrustor`` tag +Usage with the ``@NamedArgumentConstructor`` tag .. code-block:: php namespace MyCompany\Annotations; - /** - * @Annotation + /** + * @Annotation * @NamedArgumentConstructor */ class Bar implements NamedArgumentConstructorAnnotation @@ -99,8 +99,8 @@ you can simplify this to: namespace MyCompany\Annotations; - /** - * @Annotation + /** + * @Annotation * @NamedArgumentConstructor */ class Bar implements NamedArgumentConstructorAnnotation @@ -109,7 +109,7 @@ you can simplify this to: } -Usage with the +Usage with the ``Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation`` interface (v1.11, deprecated as of v1.12): .. code-block:: php diff --git a/docs/en/index.rst b/docs/en/index.rst index 95476c313..a6e338333 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -1,3 +1,12 @@ +Deprecation notice +================== + +PHP 8 introduced `attributes +`_, +which are a native replacement for annotations. As such, this library is +considered feature complete, and should receive exclusively bugfixes and +security fixes. + Introduction ============ @@ -64,10 +73,6 @@ annotations of a class. A common one is .. code-block:: php use Doctrine\Common\Annotations\AnnotationReader; - use Doctrine\Common\Annotations\AnnotationRegistry; - - // Deprecated and will be removed in 2.0 but currently needed - AnnotationRegistry::registerLoader('class_exists'); $reflectionClass = new ReflectionClass(Foo::class); $property = $reflectionClass->getProperty('bar'); @@ -80,10 +85,6 @@ annotations of a class. A common one is echo $myAnnotation->myProperty; // result: "value" -Note that ``AnnotationRegistry::registerLoader('class_exists')`` only works -if you already have an autoloader configured (i.e. composer autoloader). -Otherwise, :ref:`please take a look to the other annotation autoload mechanisms `. - A reader has multiple methods to access the annotations of a class or function. diff --git a/lib/Doctrine/Common/Annotations/Annotation.php b/lib/Doctrine/Common/Annotations/Annotation.php index 750270e42..fba23e9f1 100644 --- a/lib/Doctrine/Common/Annotations/Annotation.php +++ b/lib/Doctrine/Common/Annotations/Annotation.php @@ -18,9 +18,7 @@ class Annotation */ public $value; - /** - * @param array $data Key-value for properties to be defined in this class. - */ + /** @param array $data Key-value for properties to be defined in this class. */ final public function __construct(array $data) { foreach ($data as $key => $value) { @@ -31,11 +29,9 @@ final public function __construct(array $data) /** * Error handler for unknown property accessor in Annotation class. * - * @param string $name Unknown property name. - * * @throws BadMethodCallException */ - public function __get($name) + public function __get(string $name) { throw new BadMethodCallException( sprintf("Unknown property '%s' on annotation '%s'.", $name, static::class) @@ -45,12 +41,11 @@ public function __get($name) /** * Error handler for unknown property mutator in Annotation class. * - * @param string $name Unknown property name. - * @param mixed $value Property value. + * @param mixed $value Property value. * * @throws BadMethodCallException */ - public function __set($name, $value) + public function __set(string $name, $value) { throw new BadMethodCallException( sprintf("Unknown property '%s' on annotation '%s'.", $name, static::class) diff --git a/lib/Doctrine/Common/Annotations/Annotation/Enum.php b/lib/Doctrine/Common/Annotations/Annotation/Enum.php index 35d6410b1..6f24d9f1b 100644 --- a/lib/Doctrine/Common/Annotations/Annotation/Enum.php +++ b/lib/Doctrine/Common/Annotations/Annotation/Enum.php @@ -34,9 +34,9 @@ final class Enum public $literal; /** - * @throws InvalidArgumentException - * * @phpstan-param array{literal?: mixed[], value: list} $values + * + * @throws InvalidArgumentException */ public function __construct(array $values) { diff --git a/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php b/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php index ae60f7d5b..97a15c257 100644 --- a/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php +++ b/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php @@ -21,9 +21,9 @@ final class IgnoreAnnotation public $names; /** - * @throws RuntimeException - * * @phpstan-param array{value: string|list} $values + * + * @throws RuntimeException */ public function __construct(array $values) { diff --git a/lib/Doctrine/Common/Annotations/Annotation/Target.php b/lib/Doctrine/Common/Annotations/Annotation/Target.php index 7fd75e2bb..ba1d489ac 100644 --- a/lib/Doctrine/Common/Annotations/Annotation/Target.php +++ b/lib/Doctrine/Common/Annotations/Annotation/Target.php @@ -56,9 +56,9 @@ final class Target public $literal; /** - * @throws InvalidArgumentException - * * @phpstan-param array{value?: string|list} $values + * + * @throws InvalidArgumentException */ public function __construct(array $values) { diff --git a/lib/Doctrine/Common/Annotations/AnnotationException.php b/lib/Doctrine/Common/Annotations/AnnotationException.php index b1ea64e6f..002ee0491 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; @@ -18,11 +19,9 @@ class AnnotationException extends Exception /** * Creates a new AnnotationException describing a Syntax error. * - * @param string $message Exception message - * * @return AnnotationException */ - public static function syntaxError($message) + public static function syntaxError(string $message) { return new self('[Syntax Error] ' . $message); } @@ -30,11 +29,9 @@ public static function syntaxError($message) /** * Creates a new AnnotationException describing a Semantical error. * - * @param string $message Exception message - * * @return AnnotationException */ - public static function semanticalError($message) + public static function semanticalError(string $message) { return new self('[Semantical Error] ' . $message); } @@ -43,23 +40,19 @@ public static function semanticalError($message) * Creates a new AnnotationException describing an error which occurred during * the creation of the annotation. * - * @param string $message - * * @return AnnotationException */ - public static function creationError($message) + public static function creationError(string $message, ?Throwable $previous = null) { - return new self('[Creation Error] ' . $message); + return new self('[Creation Error] ' . $message, 0, $previous); } /** * Creates a new AnnotationException describing a type error. * - * @param string $message - * * @return AnnotationException */ - public static function typeError($message) + public static function typeError(string $message) { return new self('[Type Error] ' . $message); } @@ -67,12 +60,9 @@ public static function typeError($message) /** * Creates a new AnnotationException describing a constant semantical error. * - * @param string $identifier - * @param string $context - * * @return AnnotationException */ - public static function semanticalErrorConstants($identifier, $context = null) + public static function semanticalErrorConstants(string $identifier, ?string $context = null) { return self::semanticalError(sprintf( "Couldn't find constant %s%s.", @@ -84,16 +74,17 @@ public static function semanticalErrorConstants($identifier, $context = null) /** * Creates a new AnnotationException describing an type error of an attribute. * - * @param string $attributeName - * @param string $annotationName - * @param string $context - * @param string $expected - * @param mixed $actual + * @param mixed $actual * * @return AnnotationException */ - public static function attributeTypeError($attributeName, $annotationName, $context, $expected, $actual) - { + public static function attributeTypeError( + string $attributeName, + string $annotationName, + string $context, + string $expected, + $actual + ) { return self::typeError(sprintf( 'Attribute "%s" of @%s declared on %s expects %s, but got %s.', $attributeName, @@ -107,15 +98,14 @@ public static function attributeTypeError($attributeName, $annotationName, $cont /** * Creates a new AnnotationException describing an required error of an attribute. * - * @param string $attributeName - * @param string $annotationName - * @param string $context - * @param string $expected - * * @return AnnotationException */ - public static function requiredError($attributeName, $annotationName, $context, $expected) - { + public static function requiredError( + string $attributeName, + string $annotationName, + string $context, + string $expected + ) { return self::typeError(sprintf( 'Attribute "%s" of @%s declared on %s expects %s. This value should not be null.', $attributeName, @@ -128,17 +118,18 @@ public static function requiredError($attributeName, $annotationName, $context, /** * Creates a new AnnotationException describing a invalid enummerator. * - * @param string $attributeName - * @param string $annotationName - * @param string $context - * @param mixed $given + * @param mixed $given + * @phpstan-param list $available * * @return AnnotationException - * - * @phpstan-param list $available */ - public static function enumeratorError($attributeName, $annotationName, $context, $available, $given) - { + public static function enumeratorError( + string $attributeName, + string $annotationName, + string $context, + array $available, + $given + ) { return new self(sprintf( '[Enum Error] Attribute "%s" of @%s declared on %s accepts only [%s], but got %s.', $attributeName, @@ -149,9 +140,7 @@ public static function enumeratorError($attributeName, $annotationName, $context )); } - /** - * @return AnnotationException - */ + /** @return AnnotationException */ public static function optimizerPlusSaveComments() { return new self( @@ -159,9 +148,7 @@ public static function optimizerPlusSaveComments() ); } - /** - * @return AnnotationException - */ + /** @return AnnotationException */ public static function optimizerPlusLoadComments() { return new self( diff --git a/lib/Doctrine/Common/Annotations/AnnotationReader.php b/lib/Doctrine/Common/Annotations/AnnotationReader.php index 1f538ee53..31f3777a8 100644 --- a/lib/Doctrine/Common/Annotations/AnnotationReader.php +++ b/lib/Doctrine/Common/Annotations/AnnotationReader.php @@ -48,20 +48,16 @@ class AnnotationReader implements Reader /** * Add a new annotation to the globally ignored annotation names with regard to exception handling. - * - * @param string $name */ - public static function addGlobalIgnoredName($name) + public static function addGlobalIgnoredName(string $name) { self::$globalIgnoredNames[$name] = true; } /** * Add a new annotation to the globally ignored annotation namespaces with regard to exception handling. - * - * @param string $namespace */ - public static function addGlobalIgnoredNamespace($namespace) + public static function addGlobalIgnoredNamespace(string $namespace) { self::$globalIgnoredNamespaces[$namespace] = true; } diff --git a/lib/Doctrine/Common/Annotations/AnnotationRegistry.php b/lib/Doctrine/Common/Annotations/AnnotationRegistry.php index 259d497dd..290e60aba 100644 --- a/lib/Doctrine/Common/Annotations/AnnotationRegistry.php +++ b/lib/Doctrine/Common/Annotations/AnnotationRegistry.php @@ -3,37 +3,10 @@ namespace Doctrine\Common\Annotations; use function array_key_exists; -use function array_merge; use function class_exists; -use function in_array; -use function is_file; -use function str_replace; -use function stream_resolve_include_path; -use function strpos; - -use const DIRECTORY_SEPARATOR; final class AnnotationRegistry { - /** - * A map of namespaces to use for autoloading purposes based on a PSR-0 convention. - * - * Contains the namespace as key and an array of directories as value. If the value is NULL - * the include path is used for checking for the corresponding file. - * - * This autoloading mechanism does not utilize the PHP autoloading but implements autoloading on its own. - * - * @var string[][]|string[]|null[] - */ - private static $autoloadNamespaces = []; - - /** - * A map of autoloader callables. - * - * @var callable[] - */ - private static $loaders = []; - /** * An array of classes which cannot be found * @@ -41,93 +14,9 @@ final class AnnotationRegistry */ private static $failedToAutoload = []; - /** - * Whenever registerFile() was used. Disables use of standard autoloader. - * - * @var bool - */ - private static $registerFileUsed = false; - public static function reset(): void { - self::$autoloadNamespaces = []; - self::$loaders = []; - self::$failedToAutoload = []; - self::$registerFileUsed = false; - } - - /** - * Registers file. - * - * @deprecated This method is deprecated and will be removed in - * doctrine/annotations 2.0. Annotations will be autoloaded in 2.0. - */ - public static function registerFile(string $file): void - { - self::$registerFileUsed = true; - - require_once $file; - } - - /** - * Adds a namespace with one or many directories to look for files or null for the include path. - * - * Loading of this namespaces will be done with a PSR-0 namespace loading algorithm. - * - * @deprecated This method is deprecated and will be removed in - * doctrine/annotations 2.0. Annotations will be autoloaded in 2.0. - * - * @phpstan-param string|list|null $dirs - */ - public static function registerAutoloadNamespace(string $namespace, $dirs = null): void - { - self::$autoloadNamespaces[$namespace] = $dirs; - } - - /** - * Registers multiple namespaces. - * - * Loading of this namespaces will be done with a PSR-0 namespace loading algorithm. - * - * @deprecated This method is deprecated and will be removed in - * doctrine/annotations 2.0. Annotations will be autoloaded in 2.0. - * - * @param string[][]|string[]|null[] $namespaces indexed by namespace name - */ - public static function registerAutoloadNamespaces(array $namespaces): void - { - self::$autoloadNamespaces = array_merge(self::$autoloadNamespaces, $namespaces); - } - - /** - * Registers an autoloading callable for annotations, much like spl_autoload_register(). - * - * NOTE: These class loaders HAVE to be silent when a class was not found! - * IMPORTANT: Loaders have to return true if they loaded a class that could contain the searched annotation class. - * - * @deprecated This method is deprecated and will be removed in - * doctrine/annotations 2.0. Annotations will be autoloaded in 2.0. - */ - public static function registerLoader(callable $callable): void - { - // Reset our static cache now that we have a new loader to work with self::$failedToAutoload = []; - self::$loaders[] = $callable; - } - - /** - * Registers an autoloading callable for annotations, if it is not already registered - * - * @deprecated This method is deprecated and will be removed in - * doctrine/annotations 2.0. Annotations will be autoloaded in 2.0. - */ - public static function registerUniqueLoader(callable $callable): void - { - if (in_array($callable, self::$loaders, true)) { - return; - } - - self::registerLoader($callable); } /** @@ -143,43 +32,7 @@ public static function loadAnnotationClass(string $class): bool return false; } - foreach (self::$autoloadNamespaces as $namespace => $dirs) { - if (strpos($class, $namespace) !== 0) { - continue; - } - - $file = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php'; - - if ($dirs === null) { - $path = stream_resolve_include_path($file); - if ($path) { - require $path; - - return true; - } - } else { - foreach ((array) $dirs as $dir) { - if (is_file($dir . DIRECTORY_SEPARATOR . $file)) { - require $dir . DIRECTORY_SEPARATOR . $file; - - return true; - } - } - } - } - - foreach (self::$loaders as $loader) { - if ($loader($class) === true) { - return true; - } - } - - if ( - self::$loaders === [] && - self::$autoloadNamespaces === [] && - self::$registerFileUsed === false && - class_exists($class) - ) { + if (class_exists($class)) { return true; } diff --git a/lib/Doctrine/Common/Annotations/CachedReader.php b/lib/Doctrine/Common/Annotations/CachedReader.php deleted file mode 100644 index c036b2dab..000000000 --- a/lib/Doctrine/Common/Annotations/CachedReader.php +++ /dev/null @@ -1,268 +0,0 @@ -> */ - private $loadedAnnotations = []; - - /** @var int[] */ - private $loadedFilemtimes = []; - - /** - * @param bool $debug - */ - public function __construct(Reader $reader, Cache $cache, $debug = false) - { - $this->delegate = $reader; - $this->cache = $cache; - $this->debug = (bool) $debug; - } - - /** - * {@inheritDoc} - */ - public function getClassAnnotations(ReflectionClass $class) - { - $cacheKey = $class->getName(); - - if (isset($this->loadedAnnotations[$cacheKey])) { - return $this->loadedAnnotations[$cacheKey]; - } - - $annots = $this->fetchFromCache($cacheKey, $class); - if ($annots === false) { - $annots = $this->delegate->getClassAnnotations($class); - $this->saveToCache($cacheKey, $annots); - } - - return $this->loadedAnnotations[$cacheKey] = $annots; - } - - /** - * {@inheritDoc} - */ - public function getClassAnnotation(ReflectionClass $class, $annotationName) - { - foreach ($this->getClassAnnotations($class) as $annot) { - if ($annot instanceof $annotationName) { - return $annot; - } - } - - return null; - } - - /** - * {@inheritDoc} - */ - public function getPropertyAnnotations(ReflectionProperty $property) - { - $class = $property->getDeclaringClass(); - $cacheKey = $class->getName() . '$' . $property->getName(); - - if (isset($this->loadedAnnotations[$cacheKey])) { - return $this->loadedAnnotations[$cacheKey]; - } - - $annots = $this->fetchFromCache($cacheKey, $class); - if ($annots === false) { - $annots = $this->delegate->getPropertyAnnotations($property); - $this->saveToCache($cacheKey, $annots); - } - - return $this->loadedAnnotations[$cacheKey] = $annots; - } - - /** - * {@inheritDoc} - */ - public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) - { - foreach ($this->getPropertyAnnotations($property) as $annot) { - if ($annot instanceof $annotationName) { - return $annot; - } - } - - return null; - } - - /** - * {@inheritDoc} - */ - public function getMethodAnnotations(ReflectionMethod $method) - { - $class = $method->getDeclaringClass(); - $cacheKey = $class->getName() . '#' . $method->getName(); - - if (isset($this->loadedAnnotations[$cacheKey])) { - return $this->loadedAnnotations[$cacheKey]; - } - - $annots = $this->fetchFromCache($cacheKey, $class); - if ($annots === false) { - $annots = $this->delegate->getMethodAnnotations($method); - $this->saveToCache($cacheKey, $annots); - } - - return $this->loadedAnnotations[$cacheKey] = $annots; - } - - /** - * {@inheritDoc} - */ - public function getMethodAnnotation(ReflectionMethod $method, $annotationName) - { - foreach ($this->getMethodAnnotations($method) as $annot) { - if ($annot instanceof $annotationName) { - return $annot; - } - } - - return null; - } - - /** - * Clears loaded annotations. - * - * @return void - */ - public function clearLoadedAnnotations() - { - $this->loadedAnnotations = []; - $this->loadedFilemtimes = []; - } - - /** - * Fetches a value from the cache. - * - * @param string $cacheKey The cache key. - * - * @return mixed The cached value or false when the value is not in cache. - */ - private function fetchFromCache($cacheKey, ReflectionClass $class) - { - $data = $this->cache->fetch($cacheKey); - if ($data !== false) { - if (! $this->debug || $this->isCacheFresh($cacheKey, $class)) { - return $data; - } - } - - return false; - } - - /** - * Saves a value to the cache. - * - * @param string $cacheKey The cache key. - * @param mixed $value The value. - * - * @return void - */ - private function saveToCache($cacheKey, $value) - { - $this->cache->save($cacheKey, $value); - if (! $this->debug) { - return; - } - - $this->cache->save('[C]' . $cacheKey, time()); - } - - /** - * Checks if the cache is fresh. - * - * @param string $cacheKey - * - * @return bool - */ - private function isCacheFresh($cacheKey, ReflectionClass $class) - { - $lastModification = $this->getLastModification($class); - if ($lastModification === 0) { - return true; - } - - return $this->cache->fetch('[C]' . $cacheKey) >= $lastModification; - } - - /** - * Returns the time the class was last modified, testing traits and parents - */ - private function getLastModification(ReflectionClass $class): int - { - $filename = $class->getFileName(); - - if (isset($this->loadedFilemtimes[$filename])) { - return $this->loadedFilemtimes[$filename]; - } - - $parent = $class->getParentClass(); - - $lastModification = max(array_merge( - [$filename ? filemtime($filename) : 0], - array_map(function (ReflectionClass $reflectionTrait): int { - return $this->getTraitLastModificationTime($reflectionTrait); - }, $class->getTraits()), - array_map(function (ReflectionClass $class): int { - return $this->getLastModification($class); - }, $class->getInterfaces()), - $parent ? [$this->getLastModification($parent)] : [] - )); - - assert($lastModification !== false); - - return $this->loadedFilemtimes[$filename] = $lastModification; - } - - private function getTraitLastModificationTime(ReflectionClass $reflectionTrait): int - { - $fileName = $reflectionTrait->getFileName(); - - if (isset($this->loadedFilemtimes[$fileName])) { - return $this->loadedFilemtimes[$fileName]; - } - - $lastModificationTime = max(array_merge( - [$fileName ? filemtime($fileName) : 0], - array_map(function (ReflectionClass $reflectionTrait): int { - return $this->getTraitLastModificationTime($reflectionTrait); - }, $reflectionTrait->getTraits()) - )); - - assert($lastModificationTime !== false); - - return $this->loadedFilemtimes[$fileName] = $lastModificationTime; - } -} diff --git a/lib/Doctrine/Common/Annotations/DocLexer.php b/lib/Doctrine/Common/Annotations/DocLexer.php index f6567c512..cbb86a708 100644 --- a/lib/Doctrine/Common/Annotations/DocLexer.php +++ b/lib/Doctrine/Common/Annotations/DocLexer.php @@ -15,6 +15,8 @@ /** * Simple lexer for docblock annotations. + * + * @template-extends AbstractLexer */ final class DocLexer extends AbstractLexer { @@ -39,7 +41,7 @@ final class DocLexer extends AbstractLexer public const T_COLON = 112; public const T_MINUS = 113; - /** @var array */ + /** @var array */ protected $noCase = [ '@' => self::T_AT, ',' => self::T_COMMA, @@ -53,7 +55,7 @@ final class DocLexer extends AbstractLexer '\\' => self::T_NAMESPACE_SEPARATOR, ]; - /** @var array */ + /** @var array */ protected $withCase = [ 'true' => self::T_TRUE, 'false' => self::T_FALSE, @@ -68,7 +70,7 @@ public function nextTokenIsAdjacent(): bool { return $this->token === null || ($this->lookahead !== null - && ($this->lookahead['position'] - $this->token['position']) === strlen($this->token['value'])); + && ($this->lookahead->position - $this->token->position) === strlen($this->token->value)); } /** diff --git a/lib/Doctrine/Common/Annotations/DocParser.php b/lib/Doctrine/Common/Annotations/DocParser.php index ae530c50f..dc06bcbf8 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; @@ -285,7 +286,7 @@ public function setIgnoredAnnotationNames(array $names) * * @return void */ - public function setIgnoredAnnotationNamespaces($ignoredAnnotationNamespaces) + public function setIgnoredAnnotationNamespaces(array $ignoredAnnotationNamespaces) { $this->ignoredAnnotationNamespaces = $ignoredAnnotationNamespaces; } @@ -293,25 +294,21 @@ public function setIgnoredAnnotationNamespaces($ignoredAnnotationNamespaces) /** * Sets ignore on not-imported annotations. * - * @param bool $bool - * * @return void */ - public function setIgnoreNotImportedAnnotations($bool) + public function setIgnoreNotImportedAnnotations(bool $bool) { - $this->ignoreNotImportedAnnotations = (bool) $bool; + $this->ignoreNotImportedAnnotations = $bool; } /** * Sets the default namespaces. * - * @param string $namespace - * * @return void * * @throws RuntimeException */ - public function addNamespace($namespace) + public function addNamespace(string $namespace) { if ($this->imports) { throw new RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); @@ -341,11 +338,9 @@ public function setImports(array $imports) /** * Sets current target context as bitmask. * - * @param int $target - * * @return void */ - public function setTarget($target) + public function setTarget(int $target) { $this->target = $target; } @@ -353,15 +348,12 @@ public function setTarget($target) /** * Parses the given docblock string for annotations. * - * @param string $input The docblock string to parse. - * @param string $context The parsing context. + * @phpstan-return list Array of annotations. If no annotations are found, an empty array is returned. * * @throws AnnotationException * @throws ReflectionException - * - * @phpstan-return list Array of annotations. If no annotations are found, an empty array is returned. */ - public function parse($input, $context = '') + public function parse(string $input, string $context = '') { $pos = $this->findInitialTokenPosition($input); if ($pos === null) { @@ -378,10 +370,8 @@ public function parse($input, $context = '') /** * Finds the first valid annotation - * - * @param string $input The docblock string to parse */ - private function findInitialTokenPosition($input): ?int + private function findInitialTokenPosition(string $input): ?int { $pos = 0; @@ -425,9 +415,9 @@ private function match(int $token): bool * If any of them matches, this method updates the lookahead token; otherwise * a syntax error is raised. * - * @throws AnnotationException - * * @phpstan-param list $tokens + * + * @throws AnnotationException */ private function matchAny(array $tokens): bool { @@ -453,7 +443,7 @@ private function syntaxError(string $expected, ?array $token = null): Annotation $message = sprintf('Expected %s, got ', $expected); $message .= $this->lexer->lookahead === null ? 'end of string' - : sprintf("'%s' at position %s", $token['value'], $token['position']); + : sprintf("'%s' at position %s", $token->value, $token->position); if (strlen($this->context)) { $message .= ' in ' . $this->context; @@ -533,8 +523,7 @@ class_exists(NamedArgumentConstructor::class); 'is_annotation' => strpos($docComment, '@Annotation') !== false, ]; - $metadata['has_named_argument_constructor'] = $metadata['has_constructor'] - && $class->implementsInterface(NamedArgumentConstructorAnnotation::class); + $metadata['has_named_argument_constructor'] = false; // verify that the class is really meant to be an annotation if ($metadata['is_annotation']) { @@ -673,17 +662,17 @@ private function collectAttributeTypeMetadata(array &$metadata, Attribute $attri /** * Annotations ::= Annotation {[ "*" ]* [Annotation]}* * + * @phpstan-return list + * * @throws AnnotationException * @throws ReflectionException - * - * @phpstan-return list */ private function Annotations(): array { $annotations = []; while ($this->lexer->lookahead !== null) { - if ($this->lexer->lookahead['type'] !== DocLexer::T_AT) { + if ($this->lexer->lookahead->type !== DocLexer::T_AT) { $this->lexer->moveNext(); continue; } @@ -691,8 +680,8 @@ private function Annotations(): array // make sure the @ is preceded by non-catchable pattern if ( $this->lexer->token !== null && - $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen( - $this->lexer->token['value'] + $this->lexer->lookahead->position === $this->lexer->token->position + strlen( + $this->lexer->token->value ) ) { $this->lexer->moveNext(); @@ -704,12 +693,12 @@ private function Annotations(): array $peek = $this->lexer->glimpse(); if ( ($peek === null) - || ($peek['type'] !== DocLexer::T_NAMESPACE_SEPARATOR && ! in_array( - $peek['type'], + || ($peek->type !== DocLexer::T_NAMESPACE_SEPARATOR && ! in_array( + $peek->type, self::$classIdentifiers, true )) - || $peek['position'] !== $this->lexer->lookahead['position'] + 1 + || $peek->position !== $this->lexer->lookahead->position + 1 ) { $this->lexer->moveNext(); continue; @@ -941,7 +930,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 +957,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])) { @@ -1165,9 +1154,7 @@ private function identifierEndsWithClassConstant(string $identifier): bool return $this->getClassConstantPositionInIdentifier($identifier) === strlen($identifier) - strlen('::class'); } - /** - * @return int|false - */ + /** @return int|false */ private function getClassConstantPositionInIdentifier(string $identifier) { return stripos($identifier, '::class'); @@ -1187,18 +1174,18 @@ private function Identifier(): string $this->lexer->moveNext(); - $className = $this->lexer->token['value']; + $className = $this->lexer->token->value; while ( $this->lexer->lookahead !== null && - $this->lexer->lookahead['position'] === ($this->lexer->token['position'] + - strlen($this->lexer->token['value'])) && + $this->lexer->lookahead->position === ($this->lexer->token->position + + strlen($this->lexer->token->value)) && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR) ) { $this->match(DocLexer::T_NAMESPACE_SEPARATOR); $this->matchAny(self::$classIdentifiers); - $className .= '\\' . $this->lexer->token['value']; + $className .= '\\' . $this->lexer->token->value; } return $className; @@ -1216,7 +1203,7 @@ private function Value() { $peek = $this->lexer->glimpse(); - if ($peek['type'] === DocLexer::T_EQUALS) { + if ($peek->type === DocLexer::T_EQUALS) { return $this->FieldAssignment(); } @@ -1245,21 +1232,21 @@ private function PlainValue() return $this->Constant(); } - switch ($this->lexer->lookahead['type']) { + switch ($this->lexer->lookahead->type) { case DocLexer::T_STRING: $this->match(DocLexer::T_STRING); - return $this->lexer->token['value']; + return $this->lexer->token->value; case DocLexer::T_INTEGER: $this->match(DocLexer::T_INTEGER); - return (int) $this->lexer->token['value']; + return (int) $this->lexer->token->value; case DocLexer::T_FLOAT: $this->match(DocLexer::T_FLOAT); - return (float) $this->lexer->token['value']; + return (float) $this->lexer->token->value; case DocLexer::T_TRUE: $this->match(DocLexer::T_TRUE); @@ -1291,7 +1278,7 @@ private function PlainValue() private function FieldAssignment(): stdClass { $this->match(DocLexer::T_IDENTIFIER); - $fieldName = $this->lexer->token['value']; + $fieldName = $this->lexer->token->value; $this->match(DocLexer::T_EQUALS); @@ -1356,24 +1343,24 @@ private function Arrayx(): array * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant * Key ::= string | integer | Constant * + * @phpstan-return array{mixed, mixed} + * * @throws AnnotationException * @throws ReflectionException - * - * @phpstan-return array{mixed, mixed} */ private function ArrayEntry(): array { $peek = $this->lexer->glimpse(); if ( - $peek['type'] === DocLexer::T_EQUALS - || $peek['type'] === DocLexer::T_COLON + $peek->type === DocLexer::T_EQUALS + || $peek->type === DocLexer::T_COLON ) { if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) { $key = $this->Constant(); } else { $this->matchAny([DocLexer::T_INTEGER, DocLexer::T_STRING]); - $key = $this->lexer->token['value']; + $key = $this->lexer->token->value; } $this->matchAny([DocLexer::T_EQUALS, DocLexer::T_COLON]); @@ -1456,4 +1443,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/lib/Doctrine/Common/Annotations/FileCacheReader.php b/lib/Doctrine/Common/Annotations/FileCacheReader.php deleted file mode 100644 index 6c6c22c3a..000000000 --- a/lib/Doctrine/Common/Annotations/FileCacheReader.php +++ /dev/null @@ -1,315 +0,0 @@ -> */ - private $loadedAnnotations = []; - - /** @var array */ - private $classNameHashes = []; - - /** @var int */ - private $umask; - - /** - * @param string $cacheDir - * @param bool $debug - * @param int $umask - * - * @throws InvalidArgumentException - */ - public function __construct(Reader $reader, $cacheDir, $debug = false, $umask = 0002) - { - if (! is_int($umask)) { - throw new InvalidArgumentException(sprintf( - 'The parameter umask must be an integer, was: %s', - gettype($umask) - )); - } - - $this->reader = $reader; - $this->umask = $umask; - - if (! is_dir($cacheDir) && ! @mkdir($cacheDir, 0777 & (~$this->umask), true)) { - throw new InvalidArgumentException(sprintf( - 'The directory "%s" does not exist and could not be created.', - $cacheDir - )); - } - - $this->dir = rtrim($cacheDir, '\\/'); - $this->debug = $debug; - } - - /** - * {@inheritDoc} - */ - public function getClassAnnotations(ReflectionClass $class) - { - if (! isset($this->classNameHashes[$class->name])) { - $this->classNameHashes[$class->name] = sha1($class->name); - } - - $key = $this->classNameHashes[$class->name]; - - if (isset($this->loadedAnnotations[$key])) { - return $this->loadedAnnotations[$key]; - } - - $path = $this->dir . '/' . strtr($key, '\\', '-') . '.cache.php'; - if (! is_file($path)) { - $annot = $this->reader->getClassAnnotations($class); - $this->saveCacheFile($path, $annot); - - return $this->loadedAnnotations[$key] = $annot; - } - - $filename = $class->getFilename(); - if ( - $this->debug - && $filename !== false - && filemtime($path) < filemtime($filename) - ) { - @unlink($path); - - $annot = $this->reader->getClassAnnotations($class); - $this->saveCacheFile($path, $annot); - - return $this->loadedAnnotations[$key] = $annot; - } - - return $this->loadedAnnotations[$key] = include $path; - } - - /** - * {@inheritDoc} - */ - public function getPropertyAnnotations(ReflectionProperty $property) - { - $class = $property->getDeclaringClass(); - if (! isset($this->classNameHashes[$class->name])) { - $this->classNameHashes[$class->name] = sha1($class->name); - } - - $key = $this->classNameHashes[$class->name] . '$' . $property->getName(); - - if (isset($this->loadedAnnotations[$key])) { - return $this->loadedAnnotations[$key]; - } - - $path = $this->dir . '/' . strtr($key, '\\', '-') . '.cache.php'; - if (! is_file($path)) { - $annot = $this->reader->getPropertyAnnotations($property); - $this->saveCacheFile($path, $annot); - - return $this->loadedAnnotations[$key] = $annot; - } - - $filename = $class->getFilename(); - if ( - $this->debug - && $filename !== false - && filemtime($path) < filemtime($filename) - ) { - @unlink($path); - - $annot = $this->reader->getPropertyAnnotations($property); - $this->saveCacheFile($path, $annot); - - return $this->loadedAnnotations[$key] = $annot; - } - - return $this->loadedAnnotations[$key] = include $path; - } - - /** - * {@inheritDoc} - */ - public function getMethodAnnotations(ReflectionMethod $method) - { - $class = $method->getDeclaringClass(); - if (! isset($this->classNameHashes[$class->name])) { - $this->classNameHashes[$class->name] = sha1($class->name); - } - - $key = $this->classNameHashes[$class->name] . '#' . $method->getName(); - - if (isset($this->loadedAnnotations[$key])) { - return $this->loadedAnnotations[$key]; - } - - $path = $this->dir . '/' . strtr($key, '\\', '-') . '.cache.php'; - if (! is_file($path)) { - $annot = $this->reader->getMethodAnnotations($method); - $this->saveCacheFile($path, $annot); - - return $this->loadedAnnotations[$key] = $annot; - } - - $filename = $class->getFilename(); - if ( - $this->debug - && $filename !== false - && filemtime($path) < filemtime($filename) - ) { - @unlink($path); - - $annot = $this->reader->getMethodAnnotations($method); - $this->saveCacheFile($path, $annot); - - return $this->loadedAnnotations[$key] = $annot; - } - - return $this->loadedAnnotations[$key] = include $path; - } - - /** - * Saves the cache file. - * - * @param string $path - * @param mixed $data - * - * @return void - */ - private function saveCacheFile($path, $data) - { - if (! is_writable($this->dir)) { - throw new InvalidArgumentException(sprintf( - <<<'EXCEPTION' -The directory "%s" is not writable. Both the webserver and the console user need access. -You can manage access rights for multiple users with "chmod +a". -If your system does not support this, check out the acl package., -EXCEPTION - , - $this->dir - )); - } - - $tempfile = tempnam($this->dir, uniqid('', true)); - - if ($tempfile === false) { - throw new RuntimeException(sprintf('Unable to create tempfile in directory: %s', $this->dir)); - } - - @chmod($tempfile, 0666 & (~$this->umask)); - - $written = file_put_contents( - $tempfile, - 'umask)); - - if (rename($tempfile, $path) === false) { - @unlink($tempfile); - - throw new RuntimeException(sprintf('Unable to rename %s to %s', $tempfile, $path)); - } - } - - /** - * {@inheritDoc} - */ - public function getClassAnnotation(ReflectionClass $class, $annotationName) - { - $annotations = $this->getClassAnnotations($class); - - foreach ($annotations as $annotation) { - if ($annotation instanceof $annotationName) { - return $annotation; - } - } - - return null; - } - - /** - * {@inheritDoc} - */ - public function getMethodAnnotation(ReflectionMethod $method, $annotationName) - { - $annotations = $this->getMethodAnnotations($method); - - foreach ($annotations as $annotation) { - if ($annotation instanceof $annotationName) { - return $annotation; - } - } - - return null; - } - - /** - * {@inheritDoc} - */ - public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) - { - $annotations = $this->getPropertyAnnotations($property); - - foreach ($annotations as $annotation) { - if ($annotation instanceof $annotationName) { - return $annotation; - } - } - - return null; - } - - /** - * Clears loaded annotations. - * - * @return void - */ - public function clearLoadedAnnotations() - { - $this->loadedAnnotations = []; - } -} diff --git a/lib/Doctrine/Common/Annotations/ImplicitlyIgnoredAnnotationNames.php b/lib/Doctrine/Common/Annotations/ImplicitlyIgnoredAnnotationNames.php index 2efeb1d22..ab27f8a5c 100644 --- a/lib/Doctrine/Common/Annotations/ImplicitlyIgnoredAnnotationNames.php +++ b/lib/Doctrine/Common/Annotations/ImplicitlyIgnoredAnnotationNames.php @@ -147,6 +147,7 @@ final class ImplicitlyIgnoredAnnotationNames // PHPStan, Psalm 'extends' => true, 'implements' => true, + 'readonly' => true, 'template' => true, 'use' => true, diff --git a/lib/Doctrine/Common/Annotations/IndexedReader.php b/lib/Doctrine/Common/Annotations/IndexedReader.php index 42e70765d..77b5b9cb2 100644 --- a/lib/Doctrine/Common/Annotations/IndexedReader.php +++ b/lib/Doctrine/Common/Annotations/IndexedReader.php @@ -38,9 +38,9 @@ public function getClassAnnotations(ReflectionClass $class) /** * {@inheritDoc} */ - public function getClassAnnotation(ReflectionClass $class, $annotation) + public function getClassAnnotation(ReflectionClass $class, $annotationName) { - return $this->delegate->getClassAnnotation($class, $annotation); + return $this->delegate->getClassAnnotation($class, $annotationName); } /** @@ -59,9 +59,9 @@ public function getMethodAnnotations(ReflectionMethod $method) /** * {@inheritDoc} */ - public function getMethodAnnotation(ReflectionMethod $method, $annotation) + public function getMethodAnnotation(ReflectionMethod $method, $annotationName) { - return $this->delegate->getMethodAnnotation($method, $annotation); + return $this->delegate->getMethodAnnotation($method, $annotationName); } /** @@ -80,20 +80,19 @@ public function getPropertyAnnotations(ReflectionProperty $property) /** * {@inheritDoc} */ - public function getPropertyAnnotation(ReflectionProperty $property, $annotation) + public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) { - return $this->delegate->getPropertyAnnotation($property, $annotation); + return $this->delegate->getPropertyAnnotation($property, $annotationName); } /** * Proxies all methods to the delegate. * - * @param string $method * @param mixed[] $args * * @return mixed */ - public function __call($method, $args) + public function __call(string $method, array $args) { return call_user_func_array([$this->delegate, $method], $args); } diff --git a/lib/Doctrine/Common/Annotations/NamedArgumentConstructorAnnotation.php b/lib/Doctrine/Common/Annotations/NamedArgumentConstructorAnnotation.php deleted file mode 100644 index 8af224c0b..000000000 --- a/lib/Doctrine/Common/Annotations/NamedArgumentConstructorAnnotation.php +++ /dev/null @@ -1,14 +0,0 @@ -ReflectionClass object. - * - * @return array A list with use statements in the form (Alias => FQN). - */ - public function parseClass(ReflectionClass $class) - { - return $this->parseUseStatements($class); - } - /** * Parse a class or function for use statements. * @@ -70,7 +56,7 @@ public function parseUseStatements($reflection): array * * @return string|null The content of the file or null if the file does not exist. */ - private function getFileContent($filename, $lineNumber) + private function getFileContent(string $filename, $lineNumber) { if (! is_file($filename)) { return null; diff --git a/lib/Doctrine/Common/Annotations/SimpleAnnotationReader.php b/lib/Doctrine/Common/Annotations/SimpleAnnotationReader.php deleted file mode 100644 index 8a78c119d..000000000 --- a/lib/Doctrine/Common/Annotations/SimpleAnnotationReader.php +++ /dev/null @@ -1,114 +0,0 @@ -parser = new DocParser(); - $this->parser->setIgnoreNotImportedAnnotations(true); - } - - /** - * Adds a namespace in which we will look for annotations. - * - * @param string $namespace - * - * @return void - */ - public function addNamespace($namespace) - { - $this->parser->addNamespace($namespace); - } - - /** - * {@inheritDoc} - */ - public function getClassAnnotations(ReflectionClass $class) - { - return $this->parser->parse($class->getDocComment(), 'class ' . $class->getName()); - } - - /** - * {@inheritDoc} - */ - public function getMethodAnnotations(ReflectionMethod $method) - { - return $this->parser->parse( - $method->getDocComment(), - 'method ' . $method->getDeclaringClass()->name . '::' . $method->getName() . '()' - ); - } - - /** - * {@inheritDoc} - */ - public function getPropertyAnnotations(ReflectionProperty $property) - { - return $this->parser->parse( - $property->getDocComment(), - 'property ' . $property->getDeclaringClass()->name . '::$' . $property->getName() - ); - } - - /** - * {@inheritDoc} - */ - public function getClassAnnotation(ReflectionClass $class, $annotationName) - { - foreach ($this->getClassAnnotations($class) as $annot) { - if ($annot instanceof $annotationName) { - return $annot; - } - } - - return null; - } - - /** - * {@inheritDoc} - */ - public function getMethodAnnotation(ReflectionMethod $method, $annotationName) - { - foreach ($this->getMethodAnnotations($method) as $annot) { - if ($annot instanceof $annotationName) { - return $annot; - } - } - - return null; - } - - /** - * {@inheritDoc} - */ - public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) - { - foreach ($this->getPropertyAnnotations($property) as $annot) { - if ($annot instanceof $annotationName) { - return $annot; - } - } - - return null; - } -} diff --git a/lib/Doctrine/Common/Annotations/TokenParser.php b/lib/Doctrine/Common/Annotations/TokenParser.php index 9605fb8dd..0534fd17c 100644 --- a/lib/Doctrine/Common/Annotations/TokenParser.php +++ b/lib/Doctrine/Common/Annotations/TokenParser.php @@ -46,10 +46,7 @@ class TokenParser */ private $pointer = 0; - /** - * @param string $contents - */ - public function __construct($contents) + public function __construct(string $contents) { $this->tokens = token_get_all($contents); @@ -73,7 +70,7 @@ public function __construct($contents) * * @return mixed[]|string|null The token if exists, null otherwise. */ - public function next($docCommentIsComment = true) + public function next(bool $docCommentIsComment = true) { for ($i = $this->pointer; $i < $this->numTokens; $i++) { $this->pointer++; @@ -151,7 +148,7 @@ public function parseUseStatement() * * @return array A list with all found use statements. */ - public function parseUseStatements($namespaceName) + public function parseUseStatements(string $namespaceName) { $statements = []; while (($token = $this->next())) { diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 038d03b00..eeadabad2 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -12,6 +12,8 @@ + + lib tests @@ -105,16 +107,6 @@ */tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassWithRequire.php - - - */tests/Doctrine/Tests/Common/Annotations/Fixtures/SingleClassLOC1000.php - */tests/Doctrine/Tests/Common/Annotations/Fixtures/NamespacedSingleClassLOC1000.php - - - - */tests/Doctrine/Tests/Common/Annotations/Fixtures/Controller.php - - */tests/Doctrine/Tests/Common/Annotations/Fixtures/SingleClassLOC1000.php @@ -156,4 +148,9 @@ */tests/Doctrine/Tests/Common/Annotations/Fixtures/functions.php + + + + */tests + diff --git a/phpstan.neon b/phpstan.neon index ad8f23951..4d2b2f1d3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,7 +3,12 @@ parameters: paths: - lib - tests - excludes_analyse: + scanFiles: + - tests/Doctrine/Tests/Common/Annotations/Fixtures/GlobalNamespacesPerFileWithClassAsFirst.php + - tests/Doctrine/Tests/Common/Annotations/Fixtures/GlobalNamespacesPerFileWithClassAsLast.php + - tests/Doctrine/Tests/Common/Annotations/Fixtures/NonNamespacedClass.php + - tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM58Entity.php + excludePaths: - tests/*/Fixtures/* - tests/Doctrine/Tests/Common/Annotations/ReservedKeywordsClasses.php - tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM58Entity.php @@ -13,6 +18,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/psalm.xml b/psalm.xml new file mode 100644 index 000000000..e6af38923 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/tests/Doctrine/Performance/Common/Annotations/PhpParserPerformanceWithShortCutBench.php b/tests/Doctrine/Performance/Common/Annotations/PhpParserPerformanceWithShortCutBench.php index 64c7a70d9..d83d9aba1 100644 --- a/tests/Doctrine/Performance/Common/Annotations/PhpParserPerformanceWithShortCutBench.php +++ b/tests/Doctrine/Performance/Common/Annotations/PhpParserPerformanceWithShortCutBench.php @@ -31,6 +31,6 @@ public function initialize(): void */ public function bench(): void { - $this->parser->parseClass($this->class); + $this->parser->parseUseStatements($this->class); } } diff --git a/tests/Doctrine/Performance/Common/Annotations/PhpParserPerformanceWithoutShortCutBench.php b/tests/Doctrine/Performance/Common/Annotations/PhpParserPerformanceWithoutShortCutBench.php index 41cb419e5..ca68000c0 100644 --- a/tests/Doctrine/Performance/Common/Annotations/PhpParserPerformanceWithoutShortCutBench.php +++ b/tests/Doctrine/Performance/Common/Annotations/PhpParserPerformanceWithoutShortCutBench.php @@ -31,6 +31,6 @@ public function initialize(): void */ public function bench(): void { - $this->parser->parseClass($this->class); + $this->parser->parseUseStatements($this->class); } } diff --git a/tests/Doctrine/Tests/Common/Annotations/AnnotationReaderTest.php b/tests/Doctrine/Tests/Common/Annotations/AnnotationReaderTest.php index 5c8754295..04ffe1b37 100644 --- a/tests/Doctrine/Tests/Common/Annotations/AnnotationReaderTest.php +++ b/tests/Doctrine/Tests/Common/Annotations/AnnotationReaderTest.php @@ -6,6 +6,8 @@ use Doctrine\Common\Annotations\DocParser; use Doctrine\Common\Annotations\Reader; use Doctrine\Tests\Common\Annotations\Fixtures\Annotation\SingleUseAnnotation; +use Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithEnumProperty; +use Doctrine\Tests\Common\Annotations\Fixtures\ClassWithEnumAnnotations; use Doctrine\Tests\Common\Annotations\Fixtures\ClassWithFullPathUseStatement; use Doctrine\Tests\Common\Annotations\Fixtures\ClassWithImportedIgnoredAnnotation; use Doctrine\Tests\Common\Annotations\Fixtures\ClassWithPHPCodeSnifferAnnotation; @@ -15,6 +17,7 @@ use Doctrine\Tests\Common\Annotations\Fixtures\IgnoredNamespaces\AnnotatedAtMethodLevel; use Doctrine\Tests\Common\Annotations\Fixtures\IgnoredNamespaces\AnnotatedAtPropertyLevel; use Doctrine\Tests\Common\Annotations\Fixtures\IgnoredNamespaces\AnnotatedWithAlias; +use Doctrine\Tests\Common\Annotations\Fixtures\Suit; use InvalidArgumentException; use LogicException; use ReflectionClass; @@ -24,6 +27,8 @@ use function spl_autoload_register; use function spl_autoload_unregister; +use const PHP_VERSION_ID; + class AnnotationReaderTest extends AbstractReaderTest { /** @@ -295,4 +300,33 @@ public function testFunctionAnnotation(): void $annotation = $reader->getFunctionAnnotation($ref, Fixtures\Annotation\Autoload::class); self::assertInstanceOf(Fixtures\Annotation\Autoload::class, $annotation); } + + /** + * @requires PHP 8.1 + * @dataProvider provideEnumProperties + */ + public function testAnnotationWithEnum(string $property, Suit $expectedValue): void + { + $reader = $this->getReader(); + $ref = new ReflectionClass(ClassWithEnumAnnotations::class); + + $annotation = $reader->getPropertyAnnotation($ref->getProperty($property), AnnotationWithEnumProperty::class); + + self::assertSame($expectedValue, $annotation->suit); + } + + /** + * @return array + */ + public function provideEnumProperties(): array + { + if (PHP_VERSION_ID < 80100) { + return []; + } + + return [ + 'annotationWithDefaults' => ['annotationWithDefaults', Suit::Hearts], + 'annotationWithSpades' => ['annotationWithSpades', Suit::Spades], + ]; + } } diff --git a/tests/Doctrine/Tests/Common/Annotations/AnnotationRegistryTest.php b/tests/Doctrine/Tests/Common/Annotations/AnnotationRegistryTest.php index 5fe6c733d..2a9bf27ea 100644 --- a/tests/Doctrine/Tests/Common/Annotations/AnnotationRegistryTest.php +++ b/tests/Doctrine/Tests/Common/Annotations/AnnotationRegistryTest.php @@ -4,202 +4,10 @@ use Doctrine\Common\Annotations\AnnotationRegistry; use Doctrine\Tests\Common\Annotations\Fixtures\Annotation\CanBeAutoLoaded; -use Doctrine\Tests\Common\Annotations\Fixtures\Annotation\LoadedUsingRegisterFile; -use Doctrine\Tests\Common\Annotations\Fixtures\Annotation\ShouldNeverBeLoaded; use PHPUnit\Framework\TestCase; -use ReflectionProperty; -use TypeError; - -use function random_int; class AnnotationRegistryTest extends TestCase { - /** @var string */ - protected $class = AnnotationRegistry::class; - - /** - * @runInSeparateProcess - */ - public function testReset(): void - { - $data = ['foo' => 'bar']; - - $this->setStaticField($this->class, 'autoloadNamespaces', $data); - $this->setStaticField($this->class, 'loaders', $data); - - self::assertSame($data, $this->getStaticField($this->class, 'autoloadNamespaces')); - self::assertSame($data, $this->getStaticField($this->class, 'loaders')); - - AnnotationRegistry::reset(); - - self::assertEmpty($this->getStaticField($this->class, 'autoloadNamespaces')); - self::assertEmpty($this->getStaticField($this->class, 'loaders')); - } - - /** - * @runInSeparateProcess - */ - public function testRegisterAutoloadNamespaces(): void - { - $this->setStaticField($this->class, 'autoloadNamespaces', ['foo' => 'bar']); - - AnnotationRegistry::registerAutoloadNamespaces(['test' => 'bar']); - self::assertSame(['foo' => 'bar', 'test' => 'bar'], $this->getStaticField($this->class, 'autoloadNamespaces')); - } - - /** - * @runInSeparateProcess - */ - public function testRegisterLoaderNoCallable(): void - { - $this->expectException(TypeError::class); - - AnnotationRegistry::registerLoader('test' . random_int(10, 10000)); - } - - /** - * @param mixed[] $value - */ - protected function setStaticField(string $class, string $field, array $value): void - { - $reflection = new ReflectionProperty($class, $field); - - $reflection->setAccessible(true); - $reflection->setValue(null, $value); - } - - /** - * @return mixed - */ - protected function getStaticField(string $class, string $field) - { - $reflection = new ReflectionProperty($class, $field); - - $reflection->setAccessible(true); - - return $reflection->getValue(); - } - - /** - * @runInSeparateProcess - */ - public function testStopCallingLoadersIfClassIsNotFound(): void - { - AnnotationRegistry::reset(); - $i = 0; - $autoLoader = static function () use (&$i): bool { - $i += 1; - - return false; - }; - AnnotationRegistry::registerLoader($autoLoader); - AnnotationRegistry::loadAnnotationClass('unloadableClass'); - AnnotationRegistry::loadAnnotationClass('unloadableClass'); - AnnotationRegistry::loadAnnotationClass('unloadableClass'); - self::assertSame(1, $i, 'Autoloader should only be called once'); - } - - /** - * @runInSeparateProcess - */ - public function testStopCallingLoadersAfterClassIsFound(): void - { - $className = 'autoloadedClass' . random_int(10, 100000); - AnnotationRegistry::reset(); - $i = 0; - $autoLoader = static function () use (&$i, $className): bool { - eval('class ' . $className . ' {}'); - $i += 1; - - return true; - }; - AnnotationRegistry::registerLoader($autoLoader); - AnnotationRegistry::loadAnnotationClass($className); - AnnotationRegistry::loadAnnotationClass($className); - AnnotationRegistry::loadAnnotationClass($className); - self::assertSame(1, $i, 'Autoloader should only be called once'); - } - - /** - * @runInSeparateProcess - */ - public function testAddingANewLoaderClearsTheCache(): void - { - $failures = 0; - $failingLoader = static function () use (&$failures): bool { - $failures += 1; - - return false; - }; - - AnnotationRegistry::reset(); - AnnotationRegistry::registerLoader($failingLoader); - - self::assertSame(0, $failures); - - AnnotationRegistry::loadAnnotationClass('unloadableClass'); - - self::assertSame(1, $failures); - - AnnotationRegistry::loadAnnotationClass('unloadableClass'); - - self::assertSame(1, $failures); - - AnnotationRegistry::registerLoader(static function (): bool { - return false; - }); - AnnotationRegistry::loadAnnotationClass('unloadableClass'); - - self::assertSame(2, $failures); - } - - /** - * @runInSeparateProcess - */ - public function testResetClearsRegisteredAutoloaderFailures(): void - { - $failures = 0; - $failingLoader = static function () use (&$failures): bool { - $failures += 1; - - return false; - }; - - AnnotationRegistry::reset(); - AnnotationRegistry::registerLoader($failingLoader); - - self::assertSame(0, $failures); - - AnnotationRegistry::loadAnnotationClass('unloadableClass'); - - self::assertSame(1, $failures); - - AnnotationRegistry::loadAnnotationClass('unloadableClass'); - - self::assertSame(1, $failures); - - AnnotationRegistry::reset(); - AnnotationRegistry::registerLoader($failingLoader); - AnnotationRegistry::loadAnnotationClass('unloadableClass'); - - self::assertSame(2, $failures); - } - - /** - * @runInSeparateProcess - */ - public function testRegisterLoaderIfNotExistsOnlyRegisteresSameLoaderOnce(): void - { - $className = 'autoloadedClassThatDoesNotExist'; - AnnotationRegistry::reset(); - $autoLoader = self::createPartialMock(Autoloader::class, ['__invoke']); - $autoLoader->expects($this->once())->method('__invoke'); - AnnotationRegistry::registerUniqueLoader($autoLoader); - AnnotationRegistry::registerUniqueLoader($autoLoader); - AnnotationRegistry::loadAnnotationClass($className); - AnnotationRegistry::loadAnnotationClass($className); - } - /** * @runInSeparateProcess */ @@ -209,23 +17,4 @@ public function testClassExistsFallback(): void self::assertTrue(AnnotationRegistry::loadAnnotationClass(CanBeAutoLoaded::class)); } - - /** - * @runInSeparateProcess - */ - public function testClassExistsFallbackNotUsedWhenRegisterFileUsed(): void - { - AnnotationRegistry::reset(); - AnnotationRegistry::registerFile(__DIR__ . '/Fixtures/Annotation/LoadedUsingRegisterFile.php'); - - self::assertTrue(AnnotationRegistry::loadAnnotationClass(LoadedUsingRegisterFile::class)); - self::assertFalse(AnnotationRegistry::loadAnnotationClass(ShouldNeverBeLoaded::class)); - } -} - -class Autoloader -{ - public function __invoke(): void - { - } } diff --git a/tests/Doctrine/Tests/Common/Annotations/CachedReaderTest.php b/tests/Doctrine/Tests/Common/Annotations/CachedReaderTest.php deleted file mode 100644 index 63537e0d4..000000000 --- a/tests/Doctrine/Tests/Common/Annotations/CachedReaderTest.php +++ /dev/null @@ -1,292 +0,0 @@ -markTestSkipped('Cannot test deprecated cached reader without doctrine/cache 1.x'); - } - - parent::setup(); - } - - public function testIgnoresStaleCache(): void - { - $cache = time() - 10; - touch(__DIR__ . '/Fixtures/Controller.php', $cache + 10); - - $this->doTestCacheStale(Fixtures\Controller::class, $cache); - } - - /** - * @group 62 - */ - public function testIgnoresStaleCacheWithParentClass(): void - { - $cache = time() - 10; - touch(__DIR__ . '/Fixtures/ControllerWithParentClass.php', $cache - 10); - touch(__DIR__ . '/Fixtures/AbstractController.php', $cache + 10); - - $this->doTestCacheStale(Fixtures\ControllerWithParentClass::class, $cache); - } - - /** - * @group 62 - */ - public function testIgnoresStaleCacheWithTraits(): void - { - $cache = time() - 10; - touch(__DIR__ . '/Fixtures/ControllerWithTrait.php', $cache - 10); - touch(__DIR__ . '/Fixtures/Traits/SecretRouteTrait.php', $cache + 10); - - $this->doTestCacheStale(Fixtures\ControllerWithTrait::class, $cache); - } - - /** - * @group 62 - */ - public function testIgnoresStaleCacheWithTraitsThatUseOtherTraits(): void - { - $cache = time() - 10; - - touch(__DIR__ . '/Fixtures/ClassThatUsesTraitThatUsesAnotherTrait.php', $cache - 10); - touch(__DIR__ . '/Fixtures/Traits/EmptyTrait.php', $cache + 10); - - $this->doTestCacheStale( - Fixtures\ClassThatUsesTraitThatUsesAnotherTrait::class, - $cache - ); - } - - /** - * @group 62 - */ - public function testIgnoresStaleCacheWithInterfacesThatExtendOtherInterfaces(): void - { - $cache = time() - 10; - - touch(__DIR__ . '/Fixtures/InterfaceThatExtendsAnInterface.php', $cache - 10); - touch(__DIR__ . '/Fixtures/EmptyInterface.php', $cache + 10); - - $this->doTestCacheStale( - Fixtures\InterfaceThatExtendsAnInterface::class, - $cache - ); - } - - /** - * @group 62 - * @group 105 - */ - public function testUsesFreshCacheWithTraitsThatUseOtherTraits(): void - { - $cacheTime = time(); - - touch(__DIR__ . '/Fixtures/ClassThatUsesTraitThatUsesAnotherTrait.php', $cacheTime - 10); - touch(__DIR__ . '/Fixtures/Traits/EmptyTrait.php', $cacheTime - 10); - - $this->doTestCacheFresh( - 'Doctrine\Tests\Common\Annotations\Fixtures\ClassThatUsesTraitThatUsesAnotherTrait', - $cacheTime - ); - } - - /** - * @group 62 - */ - public function testPurgeLoadedAnnotations(): void - { - $cache = time() - 10; - - touch(__DIR__ . '/Fixtures/ClassThatUsesTraitThatUsesAnotherTrait.php', $cache - 10); - touch(__DIR__ . '/Fixtures/Traits/EmptyTrait.php', $cache + 10); - - $reader = $this->doTestCacheStale( - Fixtures\ClassThatUsesTraitThatUsesAnotherTrait::class, - $cache - ); - - $classReader = new ReflectionClass(CachedReader::class); - - $loadedAnnotationsProperty = $classReader->getProperty('loadedAnnotations'); - $loadedAnnotationsProperty->setAccessible(true); - $this->assertCount(1, $loadedAnnotationsProperty->getValue($reader)); - - $loadedFilemtimesProperty = $classReader->getProperty('loadedFilemtimes'); - $loadedFilemtimesProperty->setAccessible(true); - $this->assertCount(3, $loadedFilemtimesProperty->getValue($reader)); - - $reader->clearLoadedAnnotations(); - - $this->assertCount(0, $loadedAnnotationsProperty->getValue($reader)); - $this->assertCount(0, $loadedFilemtimesProperty->getValue($reader)); - } - - /** - * As there is a cache on loadedAnnotations, we need to test two different - * method's annotations of the same file - * - * We test four things - * 1. we load the file (and its filemtime) for method1 annotation with fresh cache - * 2. we load the file for method2 with stale cache => but still no save, because seen as fresh - * 3. we purge loaded annotations and filemtime - * 4. same as 2, but this time without filemtime cache, so file seen as stale and new cache is saved - * - * @group 62 - * @group 105 - */ - public function testAvoidCallingFilemtimeTooMuch(): void - { - $this->markTestSkipped('Skipped until further investigation'); - - $className = ClassThatUsesTraitThatUsesAnotherTraitWithMethods::class; - $cacheKey = $className; - $cacheTime = time() - 10; - - $cacheKeyMethod1 = $cacheKey . '#method1'; - $cacheKeyMethod2 = $cacheKey . '#method2'; - - $route1 = new Route(); - $route1->pattern = '/someprefix'; - $route2 = new Route(); - $route2->pattern = '/someotherprefix'; - - $cache = $this->createMock('Doctrine\Common\Cache\Cache'); - assert($cache instanceof Cache && $cache instanceof MockObject); - - $cache - ->expects($this->exactly(6)) - ->method('fetch') - ->withConsecutive( - // first pass => cache ok for method 1 - // we load annotations AND filemtimes for this file - [$this->equalTo($cacheKeyMethod1)], - [$this->equalTo('[C]' . $cacheKeyMethod1)], - // second pass => cache ok for method 2 - // filemtime is seen as fresh even if it's not - [$this->equalTo($cacheKeyMethod2)], - [$this->equalTo('[C]' . $cacheKeyMethod2)], - // third pass => cache stale for method 2 - // filemtime is seen as not fresh => we save - [$this->equalTo($cacheKeyMethod2)], - [$this->equalTo('[C]' . $cacheKeyMethod2)] - ) - ->willReturnOnConsecutiveCalls( - [$route1], // Result was cached, but there was an annotation; - $cacheTime, - [$route2], // Result was cached, but there was an annotation; - $cacheTime, - [$route2], // Result was cached, but there was an annotation; - $cacheTime - ); - - $cache - ->expects($this->exactly(2)) - ->method('save') - ->withConsecutive( - [$this->equalTo($cacheKeyMethod2)], - [$this->equalTo('[C]' . $cacheKeyMethod2)] - ); - - $reader = new CachedReader(new AnnotationReader(), $cache, true); - - touch(__DIR__ . '/Fixtures/ClassThatUsesTraitThatUsesAnotherTraitWithMethods.php', $cacheTime - 20); - touch(__DIR__ . '/Fixtures/Traits/EmptyTrait.php', $cacheTime - 20); - $this->assertEquals([$route1], $reader->getMethodAnnotations(new ReflectionMethod($className, 'method1'))); - - // only filemtime changes, but not cleared => no change - touch(__DIR__ . '/Fixtures/ClassThatUsesTraitThatUsesAnotherTrait.php', $cacheTime + 5); - touch(__DIR__ . '/Fixtures/Traits/EmptyTrait.php', $cacheTime + 5); - $this->assertEquals([$route2], $reader->getMethodAnnotations(new ReflectionMethod($className, 'method2'))); - - $reader->clearLoadedAnnotations(); - $this->assertEquals([$route2], $reader->getMethodAnnotations(new ReflectionMethod($className, 'method2'))); - } - - protected function doTestCacheStale(string $className, int $lastCacheModification): CachedReader - { - $cacheKey = $className; - - $cache = $this->createMock(Cache::class); - $cache - ->expects($this->exactly(2)) - ->method('fetch') - ->withConsecutive( - [$this->equalTo($cacheKey)], - [$this->equalTo('[C]' . $cacheKey)] - ) - ->willReturnOnConsecutiveCalls( - [], // Result was cached, but there was no annotation - $lastCacheModification - ); - $cache - ->expects($this->exactly(2)) - ->method('save') - ->withConsecutive( - [$this->equalTo($cacheKey)], - [$this->equalTo('[C]' . $cacheKey)] - ); - - $reader = new CachedReader(new AnnotationReader(), $cache, true); - $route = new Route(); - $route->pattern = '/someprefix'; - - self::assertEquals([$route], $reader->getClassAnnotations(new ReflectionClass($className))); - - return $reader; - } - - protected function doTestCacheFresh(string $className, int $lastCacheModification): void - { - $cacheKey = $className; - $route = new Route(); - $route->pattern = '/someprefix'; - - $cache = $this->createMock('Doctrine\Common\Cache\Cache'); - $cache - ->expects($this->exactly(2)) - ->method('fetch') - ->withConsecutive( - [$this->equalTo($cacheKey)], - [$this->equalTo('[C]' . $cacheKey)] - ) - ->willReturnOnConsecutiveCalls( - [$route], - $lastCacheModification - ); - $cache->expects(self::never())->method('save'); - - $reader = new CachedReader(new AnnotationReader(), $cache, true); - - $this->assertEquals([$route], $reader->getClassAnnotations(new ReflectionClass($className))); - } - - protected function getReader(): Reader - { - $this->cache = new ArrayCache(); - - return new CachedReader(new AnnotationReader(), $this->cache); - } -} diff --git a/tests/Doctrine/Tests/Common/Annotations/DocLexerTest.php b/tests/Doctrine/Tests/Common/Annotations/DocLexerTest.php index 0df8a115a..f0bc8b080 100644 --- a/tests/Doctrine/Tests/Common/Annotations/DocLexerTest.php +++ b/tests/Doctrine/Tests/Common/Annotations/DocLexerTest.php @@ -3,6 +3,7 @@ namespace Doctrine\Tests\Common\Annotations; use Doctrine\Common\Annotations\DocLexer; +use Doctrine\Common\Lexer\Token; use PHPUnit\Framework\TestCase; use function str_repeat; @@ -19,11 +20,13 @@ public function testMarkerAnnotation(): void self::assertTrue($lexer->moveNext()); self::assertNull($lexer->token); - self::assertEquals('@', $lexer->lookahead['value']); + self::assertNotNull($lexer->lookahead); + self::assertEquals('@', $lexer->lookahead->value); self::assertTrue($lexer->moveNext()); - self::assertEquals('@', $lexer->token['value']); - self::assertEquals('Name', $lexer->lookahead['value']); + self::assertNotNull($lexer->token); + self::assertEquals('@', $lexer->token->value); + self::assertEquals('Name', $lexer->lookahead->value); self::assertFalse($lexer->moveNext()); } @@ -112,9 +115,9 @@ public function testScannerTokenizesDocBlockWhitConstants(): void foreach ($tokens as $expected) { $lexer->moveNext(); $lookahead = $lexer->lookahead; - self::assertEquals($expected['value'], $lookahead['value']); - self::assertEquals($expected['type'], $lookahead['type']); - self::assertEquals($expected['position'], $lookahead['position']); + self::assertEquals($expected['value'], $lookahead->value); + self::assertEquals($expected['type'], $lookahead->type); + self::assertEquals($expected['position'], $lookahead->position); } self::assertFalse($lexer->moveNext()); @@ -153,9 +156,9 @@ public function testScannerTokenizesDocBlockWhitInvalidIdentifier(): void foreach ($tokens as $expected) { $lexer->moveNext(); $lookahead = $lexer->lookahead; - self::assertEquals($expected['value'], $lookahead['value']); - self::assertEquals($expected['type'], $lookahead['type']); - self::assertEquals($expected['position'], $lookahead['position']); + self::assertEquals($expected['value'], $lookahead->value); + self::assertEquals($expected['type'], $lookahead->type); + self::assertEquals($expected['position'], $lookahead->position); } self::assertFalse($lexer->moveNext()); @@ -170,7 +173,7 @@ public function testWithinDoubleQuotesVeryVeryLongStringWillNotOverflowPregSplit $lexer->setInput('"' . str_repeat('.', 20240) . '"'); - self::assertIsArray($lexer->glimpse()); + self::assertInstanceOf(Token::class, $lexer->glimpse()); } /** @@ -214,9 +217,9 @@ public function testRecognizesDoubleQuotesEscapeSequence(): void foreach ($tokens as $expected) { $lexer->moveNext(); $lookahead = $lexer->lookahead; - self::assertEquals($expected['value'], $lookahead['value']); - self::assertEquals($expected['type'], $lookahead['type']); - self::assertEquals($expected['position'], $lookahead['position']); + self::assertEquals($expected['value'], $lookahead->value); + self::assertEquals($expected['type'], $lookahead->type); + self::assertEquals($expected['position'], $lookahead->position); } self::assertFalse($lexer->moveNext()); @@ -294,9 +297,9 @@ private function expectDocblockTokens(string $docBlock, array $expectedTokens): $lookahead = $lexer->lookahead; $actualTokens[] = [ - 'value' => $lookahead['value'], - 'type' => $lookahead['type'], - 'position' => $lookahead['position'], + 'value' => $lookahead->value, + 'type' => $lookahead->type, + 'position' => $lookahead->position, ]; } @@ -318,4 +321,9 @@ public function testTokenAdjacency(): void self::assertFalse($lexer->nextTokenIsAdjacent()); self::assertFalse($lexer->moveNext()); } + + public function testItReturnsNullWhenThereIsNothingToParse(): void + { + self::assertNull((new DocLexer())->peek()); + } } diff --git a/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php b/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php index fad9bf93b..9037936f0 100644 --- a/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php +++ b/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php @@ -6,23 +6,25 @@ use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use Doctrine\Common\Annotations\Annotation\Target; use Doctrine\Common\Annotations\AnnotationException; -use Doctrine\Common\Annotations\AnnotationRegistry; use Doctrine\Common\Annotations\DocParser; -use Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation; use Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll; use Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithConstants; 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; @@ -126,7 +128,7 @@ public function testBasicAnnotations(): void self::assertEquals('value2', $annot->value[1]['key2']); // Complete docblock - $docblock = <<setIgnoreNotImportedAnnotations(true); - $docblock = <<createTestParser(); - $docblock = <<createTestParser(); - $docblock = <<data); self::assertEquals($annot->data, 'Some data'); - $docblock = <<name, 'Some Name'); self::assertEquals($annot->data, 'Some data'); - $docblock = <<data, 'Some data'); self::assertNull($annot->name); - $docblock = <<name, 'Some name'); self::assertNull($annot->data); - $docblock = <<data, 'Some data'); self::assertNull($annot->name); - $docblock = <<name, 'Some name'); self::assertEquals($annot->data, 'Some data'); - $docblock = <<name, 'Some name'); self::assertEquals($annot->data, 'Some data'); - $docblock = <<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 +906,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; + } } /** @@ -1037,7 +1056,7 @@ public function testSupportClassConstants(string $docblock, $expected): void public function testWithoutConstructorWhenIsNotDefaultValue(): void { $parser = $this->createTestParser(); - $docblock = <<createTestParser(); - $docblock = <<createTestParser(); $context = 'class SomeClassName'; - $docblock = <<createTestParser(); $context = 'class SomeClassName'; - $docblock = <<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 { $parser = $this->createTestParser(); $context = 'class SomeClassName'; - $docblock = <<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; + } } /** @@ -1130,7 +1169,7 @@ public function testRegressionDDC575(): void { $parser = $this->createTestParser(); - $docblock = <<setImports([ 'autoload' => Fixtures\Annotation\Autoload::class, ]); @@ -1318,7 +1352,7 @@ public function testSyntaxErrorWithContextDescription(): void */ public function testSyntaxErrorWithUnknownCharacters(): void { - $docblock = <<markTestSkipped('This test requires mbstring function overloading is turned on'); } - $docblock = <<createTestParser() - ->parse('/** @NamedAnnotation(foo="baz", bar=2222) */'); - - self::assertCount(1, $result); - self::assertInstanceOf(NamedAnnotation::class, $result[0]); - self::assertSame('baz', $result[0]->getFoo()); - self::assertSame(2222, $result[0]->getBar()); - } - - public function testNamedReorderedArgumentsConstructorInterface(): void - { - $result = $this - ->createTestParser() - ->parse('/** @NamedAnnotation(bar=2222, foo="baz") */'); - - self::assertCount(1, $result); - self::assertInstanceOf(NamedAnnotation::class, $result[0]); - self::assertSame('baz', $result[0]->getFoo()); - self::assertSame(2222, $result[0]->getBar()); - } - - public function testNamedArgumentsConstructorInterfaceWithDefaultValue(): void - { - $result = $this - ->createTestParser() - ->parse('/** @NamedAnnotation(foo="baz") */'); - - self::assertCount(1, $result); - self::assertInstanceOf(NamedAnnotation::class, $result[0]); - self::assertSame('baz', $result[0]->getFoo()); - self::assertSame(1234, $result[0]->getBar()); - } - public function testNamedArgumentsConstructorAnnotation(): void { $result = $this @@ -1683,30 +1681,36 @@ public function testNamedArgumentsConstructorAnnotationWithInvalidArguments(): v ); $parser->parse('/** @AnotherNamedAnnotation("foo", bar=666, "hey") */'); } -} -/** @Annotation */ -class NamedAnnotation implements NamedArgumentConstructorAnnotation -{ - /** @var string */ - private $foo; - /** @var int */ - private $bar; - - public function __construct(string $foo, int $bar = 1234) + public function testNamedArgumentsConstructorAnnotationWithWrongArgumentType(): void { - $this->foo = $foo; - $this->bar = $bar; - } + $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()); - public function getFoo(): string - { - return $this->foo; + throw $exc; + } } - public function getBar(): int + /** + * Override for BC with PHPUnit <8 + */ + public function expectExceptionMessageMatches(string $regularExpression): void { - return $this->bar; + if (method_exists(get_parent_class($this), 'expectExceptionMessageMatches')) { + parent::expectExceptionMessageMatches($regularExpression); + } else { + parent::expectExceptionMessageRegExp($regularExpression); + } } } diff --git a/tests/Doctrine/Tests/Common/Annotations/FileCacheReaderTest.php b/tests/Doctrine/Tests/Common/Annotations/FileCacheReaderTest.php deleted file mode 100644 index a92f245a0..000000000 --- a/tests/Doctrine/Tests/Common/Annotations/FileCacheReaderTest.php +++ /dev/null @@ -1,56 +0,0 @@ -cacheDir = sys_get_temp_dir() . '/annotations_' . uniqid('', true); - @mkdir($this->cacheDir); - - return new FileCacheReader(new AnnotationReader(), $this->cacheDir); - } - - public function tearDown(): void - { - foreach (glob($this->cacheDir . '/*.php') as $file) { - unlink($file); - } - - rmdir($this->cacheDir); - } - - /** - * @group DCOM-81 - */ - public function testAttemptToCreateAnnotationCacheDir(): void - { - $this->cacheDir = sys_get_temp_dir() . '/not_existed_dir_' . uniqid('', true); - - if (method_exists($this, 'assertDirectoryDoesNotExist')) { - self::assertDirectoryDoesNotExist($this->cacheDir); - } else { - self::assertDirectoryNotExists($this->cacheDir); - } - - new FileCacheReader(new AnnotationReader(), $this->cacheDir); - - self::assertDirectoryExists($this->cacheDir); - } -} diff --git a/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithEnumProperty.php b/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithEnumProperty.php new file mode 100644 index 000000000..97d299769 --- /dev/null +++ b/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithEnumProperty.php @@ -0,0 +1,18 @@ + __NAMESPACE__ . '\Fixtures\Annotation\Route', 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } public function testParseClassWithMultipleImportsInUseStatement(): void @@ -32,7 +32,7 @@ public function testParseClassWithMultipleImportsInUseStatement(): void self::assertEquals([ 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } /** @@ -47,13 +47,13 @@ public function testParseClassWithGroupUseStatement(): void 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', 'supersecure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } public function testParseClassWhenNotUserDefined(): void { $parser = new PhpParser(); - self::assertEquals([], $parser->parseClass(new ReflectionClass(stdClass::class))); + self::assertEquals([], $parser->parseUseStatements(new ReflectionClass(stdClass::class))); } public function testClassFileDoesNotExist(): void @@ -64,7 +64,7 @@ public function testClassFileDoesNotExist(): void ->will($this->returnValue('/valid/class/Fake.php(35) : eval()d code')); $parser = new PhpParser(); - self::assertEquals([], $parser->parseClass($class)); + self::assertEquals([], $parser->parseUseStatements($class)); } public function testParseClassWhenClassIsNotNamespaced(): void @@ -75,7 +75,7 @@ public function testParseClassWhenClassIsNotNamespaced(): void self::assertEquals([ 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } public function testParseClassWhenClassIsInterface(): void @@ -85,7 +85,7 @@ public function testParseClassWhenClassIsInterface(): void self::assertEquals([ 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } public function testClassWithFullyQualifiedUseStatements(): void @@ -97,7 +97,7 @@ public function testClassWithFullyQualifiedUseStatements(): void 'secure' => '\\' . __NAMESPACE__ . '\Fixtures\Annotation\Secure', 'route' => '\\' . __NAMESPACE__ . '\Fixtures\Annotation\Route', 'template' => '\\' . __NAMESPACE__ . '\Fixtures\Annotation\Template', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } public function testNamespaceAndClassCommentedOut(): void @@ -108,7 +108,7 @@ public function testNamespaceAndClassCommentedOut(): void self::assertEquals([ 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } public function testEqualNamespacesPerFileWithClassAsFirst(): void @@ -119,7 +119,7 @@ public function testEqualNamespacesPerFileWithClassAsFirst(): void self::assertEquals([ 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } public function testEqualNamespacesPerFileWithClassAsLast(): void @@ -130,7 +130,7 @@ public function testEqualNamespacesPerFileWithClassAsLast(): void self::assertEquals([ 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } public function testDifferentNamespacesPerFileWithClassAsFirst(): void @@ -140,7 +140,7 @@ public function testDifferentNamespacesPerFileWithClassAsFirst(): void self::assertEquals([ 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } public function testDifferentNamespacesPerFileWithClassAsLast(): void @@ -150,7 +150,7 @@ public function testDifferentNamespacesPerFileWithClassAsLast(): void self::assertEquals([ 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } public function testGlobalNamespacesPerFileWithClassAsFirst(): void @@ -161,7 +161,7 @@ public function testGlobalNamespacesPerFileWithClassAsFirst(): void self::assertEquals([ 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } public function testGlobalNamespacesPerFileWithClassAsLast(): void @@ -172,7 +172,7 @@ public function testGlobalNamespacesPerFileWithClassAsLast(): void self::assertEquals([ 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } public function testNamespaceWithClosureDeclaration(): void @@ -184,7 +184,7 @@ public function testNamespaceWithClosureDeclaration(): void 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } public function testIfPointerResetsOnMultipleParsingTries(): void @@ -196,13 +196,13 @@ public function testIfPointerResetsOnMultipleParsingTries(): void 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); self::assertEquals([ 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } /** @@ -217,6 +217,6 @@ public function testClassWithClosure(): void self::assertEquals([ 'annotationtargetall' => __NAMESPACE__ . '\Fixtures\AnnotationTargetAll', 'annotationtargetannotation' => __NAMESPACE__ . '\Fixtures\AnnotationTargetAnnotation', - ], $parser->parseClass($class)); + ], $parser->parseUseStatements($class)); } } diff --git a/tests/Doctrine/Tests/Common/Annotations/SimpleAnnotationReaderTest.php b/tests/Doctrine/Tests/Common/Annotations/SimpleAnnotationReaderTest.php deleted file mode 100644 index f65861c60..000000000 --- a/tests/Doctrine/Tests/Common/Annotations/SimpleAnnotationReaderTest.php +++ /dev/null @@ -1,140 +0,0 @@ -ignoreIssues(); - parent::testImportDetectsNotImportedAnnotation(); - } - - /** - * Contrary to the behavior of the default annotation reader, we do just ignore - * these in the simple annotation reader (so, no expected exception here). - * - * @doesNotPerformAssertions - */ - public function testImportDetectsNonExistentAnnotation(): void - { - $this->ignoreIssues(); - parent::testImportDetectsNonExistentAnnotation(); - } - - /** - * Contrary to the behavior of the default annotation reader, we do just ignore - * these in the simple annotation reader (so, no expected exception here). - * - * @doesNotPerformAssertions - */ - public function testClassWithInvalidAnnotationTargetAtClassDocBlock(): void - { - $this->ignoreIssues(); - parent::testClassWithInvalidAnnotationTargetAtClassDocBlock(); - } - - /** - * Contrary to the behavior of the default annotation reader, we do just ignore - * these in the simple annotation reader (so, no expected exception here). - * - * @doesNotPerformAssertions - */ - public function testClassWithInvalidAnnotationTargetAtPropertyDocBlock(): void - { - $this->ignoreIssues(); - parent::testClassWithInvalidAnnotationTargetAtPropertyDocBlock(); - } - - /** - * Contrary to the behavior of the default annotation reader, we do just ignore - * these in the simple annotation reader (so, no expected exception here). - * - * @doesNotPerformAssertions - */ - public function testClassWithInvalidNestedAnnotationTargetAtPropertyDocBlock(): void - { - $this->ignoreIssues(); - parent::testClassWithInvalidNestedAnnotationTargetAtPropertyDocBlock(); - } - - /** - * Contrary to the behavior of the default annotation reader, we do just ignore - * these in the simple annotation reader (so, no expected exception here). - * - * @doesNotPerformAssertions - */ - public function testClassWithInvalidAnnotationTargetAtMethodDocBlock(): void - { - $this->ignoreIssues(); - parent::testClassWithInvalidAnnotationTargetAtMethodDocBlock(); - } - - /** - * Contrary to the behavior of the default annotation reader, we do just ignore - * these in the simple annotation reader (so, no expected exception here). - * - * @doesNotPerformAssertions - */ - public function testErrorWhenInvalidAnnotationIsUsed(): void - { - $this->ignoreIssues(); - parent::testErrorWhenInvalidAnnotationIsUsed(); - } - - /** - * The SimpleAnnotationReader doens't include the @IgnoreAnnotation in the results. - */ - public function testInvalidAnnotationUsageButIgnoredClass(): void - { - $reader = $this->getReader(); - $ref = new ReflectionClass(Fixtures\InvalidAnnotationUsageButIgnoredClass::class); - $annots = $reader->getClassAnnotations($ref); - - self::assertCount(1, $annots); - } - - public function testIncludeIgnoreAnnotation(): void - { - $this->markTestSkipped('The simplified annotation reader would always autoload annotations'); - } - - /** - * @group DDC-1660 - * @group regression - * - * Contrary to the behavior of the default annotation reader, @version is not ignored - */ - public function testInvalidAnnotationButIgnored(): void - { - $reader = $this->getReader(); - $class = new ReflectionClass(Fixtures\ClassDDC1660::class); - - self::assertTrue(class_exists(Fixtures\Annotation\Version::class)); - self::assertCount(1, $reader->getClassAnnotations($class)); - self::assertCount(1, $reader->getMethodAnnotations($class->getMethod('bar'))); - self::assertCount(1, $reader->getPropertyAnnotations($class->getProperty('foo'))); - } - - protected function getReader(): Reader - { - $reader = new SimpleAnnotationReader(); - $reader->addNamespace(__NAMESPACE__); - $reader->addNamespace(__NAMESPACE__ . '\Fixtures'); - $reader->addNamespace(__NAMESPACE__ . '\Fixtures\Annotation'); - - return $reader; - } -} diff --git a/tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM58Test.php b/tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM58Test.php index 599b62c72..6ea53287e 100644 --- a/tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM58Test.php +++ b/tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM58Test.php @@ -4,7 +4,6 @@ use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\DocParser; -use Doctrine\Common\Annotations\SimpleAnnotationReader; use PHPUnit\Framework\TestCase; use ReflectionClass; @@ -78,16 +77,6 @@ public function testIssueWithNamespacesOrImports(): void self::assertCount(1, $annots); self::assertInstanceOf(\Entity::class, $annots[0]); } - - public function testIssueSimpleAnnotationReader(): void - { - $reader = new SimpleAnnotationReader(); - $reader->addNamespace('Doctrine\Tests\Common\Annotations\Ticket\Doctrine\ORM\Mapping'); - $annots = $reader->getClassAnnotations(new ReflectionClass(__NAMESPACE__ . '\MappedClass')); - - self::assertCount(1, $annots); - self::assertInstanceOf(Doctrine\ORM\Mapping\Entity::class, $annots[0]); - } } /** diff --git a/tests/Doctrine/Tests/TestInit.php b/tests/Doctrine/Tests/TestInit.php index 27b04a1b6..f704c23f5 100644 --- a/tests/Doctrine/Tests/TestInit.php +++ b/tests/Doctrine/Tests/TestInit.php @@ -1,7 +1,5 @@