From 4fb3f1874bf7d4655155906b91b979c944169deb Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 9 Mar 2026 06:32:03 +0100 Subject: [PATCH 1/7] Replace doctrine/annotations with phpstan/phpdoc-parser --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 23067f3..10c604c 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "nikic/php-parser": "^5.0", "symfony/finder": "^5.4 || ^6.4 || ^7.0", "twig/twig": "^2.0 || ^3.0", - "doctrine/annotations": "^1.7 || ^2.0" + "phpstan/phpdoc-parser": "^2.3" }, "require-dev": { "symfony/phpunit-bridge": "^5.4 || ^6.4 || ^7.0", From 7a89dd27d586e69005fd217481e65b7c83775673 Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 9 Mar 2026 09:05:45 +0100 Subject: [PATCH 2/7] Remove custom boostrap.php --- phpunit.xml.dist | 2 +- tests/bootstrap.php | 22 ---------------------- 2 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 tests/bootstrap.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f622df9..09455da 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,7 +8,7 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - bootstrap="./tests/bootstrap.php" + bootstrap="./vendor/autoload.php" > diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index 92c0fac..0000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -use Doctrine\Common\Annotations\AnnotationRegistry; - -require __DIR__.'/../vendor/autoload.php'; - -if (!class_exists(AnnotationRegistry::class, false) && class_exists(AnnotationRegistry::class)) { - if (method_exists(AnnotationRegistry::class, 'registerUniqueLoader')) { - AnnotationRegistry::registerUniqueLoader('class_exists'); - } elseif (method_exists(AnnotationRegistry::class, 'registerLoader')) { - AnnotationRegistry::registerLoader('class_exists'); - } -} From b09fb37e206b05e45fcfecd047fd3952442e71a0 Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 9 Mar 2026 09:11:34 +0100 Subject: [PATCH 3/7] Remove annotations --- src/Annotation/Desc.php | 23 ---------------------- src/Annotation/Ignore.php | 19 ------------------ src/Annotation/Translate.php | 38 ------------------------------------ 3 files changed, 80 deletions(-) delete mode 100644 src/Annotation/Desc.php delete mode 100644 src/Annotation/Ignore.php delete mode 100644 src/Annotation/Translate.php diff --git a/src/Annotation/Desc.php b/src/Annotation/Desc.php deleted file mode 100644 index 20ebbad..0000000 --- a/src/Annotation/Desc.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Translation\Extractor\Annotation; - -/** - * @Annotation - */ -final class Desc -{ - /** - * @var string - */ - public $text; -} diff --git a/src/Annotation/Ignore.php b/src/Annotation/Ignore.php deleted file mode 100644 index a95a0c6..0000000 --- a/src/Annotation/Ignore.php +++ /dev/null @@ -1,19 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Translation\Extractor\Annotation; - -/** - * @Annotation - */ -final class Ignore -{ -} diff --git a/src/Annotation/Translate.php b/src/Annotation/Translate.php deleted file mode 100644 index b8673a2..0000000 --- a/src/Annotation/Translate.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Translation\Extractor\Annotation; - -/** - * @Annotation - */ -class Translate -{ - /** - * @var string - */ - private $domain = 'messages'; - - /** - * Translate constructor. - */ - public function __construct(array $values) - { - if (isset($values['domain'])) { - $this->domain = $values['domain']; - } - } - - public function getDomain(): string - { - return $this->domain; - } -} From 864d1eba48a4ffd5c01fc9d5782315097c70002f Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 9 Mar 2026 09:12:34 +0100 Subject: [PATCH 4/7] Use PhpDocParser from phpstan instead of DocParser from doctrine/annotations --- src/Visitor/BaseVisitor.php | 58 +++++++++++-------- src/Visitor/Php/Symfony/FormTypeChoices.php | 24 +++----- .../Php/Symfony/ValidationAnnotation.php | 11 +--- .../Php/TranslateAnnotationVisitor.php | 40 ++++++------- 4 files changed, 64 insertions(+), 69 deletions(-) diff --git a/src/Visitor/BaseVisitor.php b/src/Visitor/BaseVisitor.php index 565cc8e..1273f7c 100644 --- a/src/Visitor/BaseVisitor.php +++ b/src/Visitor/BaseVisitor.php @@ -11,11 +11,16 @@ namespace Translation\Extractor\Visitor; -use Doctrine\Common\Annotations\DocParser; use PhpParser\Node; +use PHPStan\PhpDocParser\Ast\ConstExpr\DoctrineConstExprStringNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineTagValueNode; +use PHPStan\PhpDocParser\Lexer\Lexer; +use PHPStan\PhpDocParser\Parser\ConstExprParser; +use PHPStan\PhpDocParser\Parser\PhpDocParser; +use PHPStan\PhpDocParser\Parser\TokenIterator; +use PHPStan\PhpDocParser\Parser\TypeParser; +use PHPStan\PhpDocParser\ParserConfig; use Symfony\Component\Finder\SplFileInfo; -use Translation\Extractor\Annotation\Desc; -use Translation\Extractor\Annotation\Ignore; use Translation\Extractor\Model\Error; use Translation\Extractor\Model\SourceCollection; use Translation\Extractor\Model\SourceLocation; @@ -27,7 +32,8 @@ */ abstract class BaseVisitor implements Visitor { - private ?DocParser $docParser = null; + protected ?Lexer $lexer = null; + protected ?PhpDocParser $phpDocParser = null; protected ?SourceCollection $collection = null; protected SplFileInfo $file; @@ -54,9 +60,11 @@ protected function addError(Node $node, string $errorMessage): void $line = $node->getAttribute('startLine'); } if (null !== $docComment) { - $context = 'file '.$file.' near line '.$line; - foreach ($this->getDocParser()->parse($docComment->getText(), $context) as $annotation) { - if ($annotation instanceof Ignore) { + $phpDocNode = $this->getPhpDocParser()->parse( + new TokenIterator($this->lexer->tokenize($docComment->getText())) + ); + foreach ($phpDocNode->getTags() as $tag) { + if ($tag->name === '@Ignore') { return; } } @@ -78,12 +86,16 @@ protected function getLocation(string $text, int $line, ?Node $node = null, arra { $file = $this->getAbsoluteFilePath(); if (null !== $node && null !== $docComment = $node->getDocComment()) { - $parserContext = 'file '.$file.' near line '.$line; - foreach ($this->getDocParser()->parse($docComment->getText(), $parserContext) as $annotation) { - if ($annotation instanceof Ignore) { + $phpDocNode = $this->getPhpDocParser()->parse( + new TokenIterator($this->lexer->tokenize($docComment->getText())) + ); + foreach ($phpDocNode->getTags() as $tag) { + if ($tag->name === '@Ignore') { return null; - } elseif ($annotation instanceof Desc) { - $context['desc'] = $annotation->text; + } elseif ($tag->name === '@Desc' && $tag->value instanceof DoctrineTagValueNode) { + if ([] !== $tag->value->annotation->arguments) { + $context['desc'] = DoctrineConstExprStringNode::unescape($tag->value->annotation->arguments[0]->value); + } } } } @@ -91,23 +103,21 @@ protected function getLocation(string $text, int $line, ?Node $node = null, arra return new SourceLocation($text, $file, $line, $context); } - private function getDocParser(): DocParser + protected function getPhpDocParser(): PhpDocParser { - if (null === $this->docParser) { - $this->docParser = new DocParser(); - - $this->docParser->setImports([ - 'ignore' => Ignore::class, - 'desc' => Desc::class, - ]); - $this->docParser->setIgnoreNotImportedAnnotations(true); + if (null === $this->phpDocParser) { + $config = new ParserConfig(usedAttributes: []); + $this->lexer = new Lexer($config); + $constExprParser = new ConstExprParser($config); + $typeParser = new TypeParser($config, $constExprParser); + $this->phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); } - return $this->docParser; + return $this->phpDocParser; } - public function setDocParser(DocParser $docParser): void + public function setPhpDocParser(PhpDocParser $phpDocParser): void { - $this->docParser = $docParser; + $this->phpDocParser = $phpDocParser; } } diff --git a/src/Visitor/Php/Symfony/FormTypeChoices.php b/src/Visitor/Php/Symfony/FormTypeChoices.php index 2e92f73..1321aff 100644 --- a/src/Visitor/Php/Symfony/FormTypeChoices.php +++ b/src/Visitor/Php/Symfony/FormTypeChoices.php @@ -11,10 +11,9 @@ namespace Translation\Extractor\Visitor\Php\Symfony; -use Doctrine\Common\Annotations\DocParser; use PhpParser\Node; use PhpParser\NodeVisitor; -use Translation\Extractor\Annotation\Ignore; +use PHPStan\PhpDocParser\Parser\TokenIterator; use Translation\Extractor\Model\SourceLocation; /** @@ -134,20 +133,15 @@ public function enterNode(Node $node): ?Node protected function isIgnored(Node $node): bool { - // because of getDocParser method is private, we have to create a new custom instance - $docParser = new DocParser(); - $docParser->setImports([ - 'ignore' => Ignore::class, - ]); - $docParser->setIgnoreNotImportedAnnotations(true); - if (null !== $docComment = $node->getDocComment()) { - foreach ($docParser->parse($docComment->getText()) as $annotation) { - if ($annotation instanceof Ignore) { - return true; - } - } + if (null === $node->getDocComment()) { + return false; } - return false; + $phpDocNode = $this->getPhpDocParser()->parse( + new TokenIterator($this->lexer->tokenize($node->getDocComment()->getText())) + ); + $ignoreTags = $phpDocNode->getTagsByName('@Ignore'); + + return count($ignoreTags) > 0; } } diff --git a/src/Visitor/Php/Symfony/ValidationAnnotation.php b/src/Visitor/Php/Symfony/ValidationAnnotation.php index 51c4617..3cd40a2 100644 --- a/src/Visitor/Php/Symfony/ValidationAnnotation.php +++ b/src/Visitor/Php/Symfony/ValidationAnnotation.php @@ -11,7 +11,6 @@ namespace Translation\Extractor\Visitor\Php\Symfony; -use Doctrine\Common\Annotations\AnnotationException; use PhpParser\Node; use PhpParser\NodeVisitor; use Symfony\Component\Validator\Mapping\ClassMetadata; @@ -60,14 +59,8 @@ public function enterNode(Node $node): ?Node return null; } - try { - /** @var ClassMetadata $metadata */ - $metadata = $this->metadataFactory->getMetadataFor($name); - } catch (AnnotationException $e) { - $this->addError($node, \sprintf('Could not parse class "%s" for annotations. %s', $this->namespace, $e->getMessage())); - - return null; - } + /** @var ClassMetadata $metadata */ + $metadata = $this->metadataFactory->getMetadataFor($name); if (!$metadata->hasConstraints() && !\count($metadata->getConstrainedProperties())) { return null; diff --git a/src/Visitor/Php/TranslateAnnotationVisitor.php b/src/Visitor/Php/TranslateAnnotationVisitor.php index e9e2af3..b7929e8 100644 --- a/src/Visitor/Php/TranslateAnnotationVisitor.php +++ b/src/Visitor/Php/TranslateAnnotationVisitor.php @@ -11,10 +11,13 @@ namespace Translation\Extractor\Visitor\Php; -use Doctrine\Common\Annotations\DocParser; use PhpParser\Comment; use PhpParser\Node; use PhpParser\NodeVisitor; +use PHPStan\PhpDocParser\Ast\ConstExpr\DoctrineConstExprStringNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; +use PHPStan\PhpDocParser\Parser\TokenIterator; use Translation\Extractor\Annotation\Translate; /** @@ -24,22 +27,6 @@ */ class TranslateAnnotationVisitor extends BasePHPVisitor implements NodeVisitor { - protected ?DocParser $translateDocParser = null; - - private function getTranslateDocParser(): DocParser - { - if (null === $this->translateDocParser) { - $this->translateDocParser = new DocParser(); - - $this->translateDocParser->setImports([ - 'translate' => Translate::class, - ]); - $this->translateDocParser->setIgnoreNotImportedAnnotations(true); - } - - return $this->translateDocParser; - } - public function enterNode(Node $node): ?Node { // look for strings @@ -58,10 +45,21 @@ public function enterNode(Node $node): ?Node return null; } - foreach ($this->getTranslateDocParser()->parse($comment->getText()) as $annotation) { - // add phrase to dictionary - $this->addLocation($node->value, $node->getAttribute('startLine'), $node, ['domain' => $annotation->getDomain()]); - + $phpDocNode = $this->getPhpDocParser()->parse( + new TokenIterator($this->lexer->tokenize($comment->getText())) + ); + + $translateTags = $phpDocNode->getTagsByName('@Translate'); + if ([] !== $translateTags) { + $domain = 'messages'; + if ($translateTags[0]->value instanceof DoctrineTagValueNode) { + foreach ($translateTags[0]->value->annotation->arguments as $argument) { + if ('domain' === $argument->key->name) { + $domain = DoctrineConstExprStringNode::unescape($argument->value); + } + } + } + $this->addLocation($node->value, $node->getAttribute('startLine'), $node, ['domain' => $domain]); break; } } From 9028af971189ea55e5a5485efb8e78aff38955cb Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 9 Mar 2026 18:55:18 +0100 Subject: [PATCH 5/7] php-cs-fixer pass --- src/Visitor/BaseVisitor.php | 6 +++--- src/Visitor/Php/Symfony/FormTypeChoices.php | 2 +- src/Visitor/Php/TranslateAnnotationVisitor.php | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Visitor/BaseVisitor.php b/src/Visitor/BaseVisitor.php index 1273f7c..8ad0102 100644 --- a/src/Visitor/BaseVisitor.php +++ b/src/Visitor/BaseVisitor.php @@ -64,7 +64,7 @@ protected function addError(Node $node, string $errorMessage): void new TokenIterator($this->lexer->tokenize($docComment->getText())) ); foreach ($phpDocNode->getTags() as $tag) { - if ($tag->name === '@Ignore') { + if ('@Ignore' === $tag->name) { return; } } @@ -90,9 +90,9 @@ protected function getLocation(string $text, int $line, ?Node $node = null, arra new TokenIterator($this->lexer->tokenize($docComment->getText())) ); foreach ($phpDocNode->getTags() as $tag) { - if ($tag->name === '@Ignore') { + if ('@Ignore' === $tag->name) { return null; - } elseif ($tag->name === '@Desc' && $tag->value instanceof DoctrineTagValueNode) { + } elseif ('@Desc' === $tag->name && $tag->value instanceof DoctrineTagValueNode) { if ([] !== $tag->value->annotation->arguments) { $context['desc'] = DoctrineConstExprStringNode::unescape($tag->value->annotation->arguments[0]->value); } diff --git a/src/Visitor/Php/Symfony/FormTypeChoices.php b/src/Visitor/Php/Symfony/FormTypeChoices.php index 1321aff..d407217 100644 --- a/src/Visitor/Php/Symfony/FormTypeChoices.php +++ b/src/Visitor/Php/Symfony/FormTypeChoices.php @@ -142,6 +142,6 @@ protected function isIgnored(Node $node): bool ); $ignoreTags = $phpDocNode->getTagsByName('@Ignore'); - return count($ignoreTags) > 0; + return \count($ignoreTags) > 0; } } diff --git a/src/Visitor/Php/TranslateAnnotationVisitor.php b/src/Visitor/Php/TranslateAnnotationVisitor.php index b7929e8..22a0fc9 100644 --- a/src/Visitor/Php/TranslateAnnotationVisitor.php +++ b/src/Visitor/Php/TranslateAnnotationVisitor.php @@ -16,7 +16,6 @@ use PhpParser\NodeVisitor; use PHPStan\PhpDocParser\Ast\ConstExpr\DoctrineConstExprStringNode; use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineTagValueNode; -use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\PhpDocParser\Parser\TokenIterator; use Translation\Extractor\Annotation\Translate; From e763301a80f746f3039cad3ea441132d68b74eda Mon Sep 17 00:00:00 2001 From: Marc Date: Wed, 11 Mar 2026 05:20:04 +0100 Subject: [PATCH 6/7] Revert "Remove annotations" This reverts commit b09fb37e206b05e45fcfecd047fd3952442e71a0. --- src/Annotation/Desc.php | 23 ++++++++++++++++++++++ src/Annotation/Ignore.php | 19 ++++++++++++++++++ src/Annotation/Translate.php | 38 ++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 src/Annotation/Desc.php create mode 100644 src/Annotation/Ignore.php create mode 100644 src/Annotation/Translate.php diff --git a/src/Annotation/Desc.php b/src/Annotation/Desc.php new file mode 100644 index 0000000..20ebbad --- /dev/null +++ b/src/Annotation/Desc.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Extractor\Annotation; + +/** + * @Annotation + */ +final class Desc +{ + /** + * @var string + */ + public $text; +} diff --git a/src/Annotation/Ignore.php b/src/Annotation/Ignore.php new file mode 100644 index 0000000..a95a0c6 --- /dev/null +++ b/src/Annotation/Ignore.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Extractor\Annotation; + +/** + * @Annotation + */ +final class Ignore +{ +} diff --git a/src/Annotation/Translate.php b/src/Annotation/Translate.php new file mode 100644 index 0000000..b8673a2 --- /dev/null +++ b/src/Annotation/Translate.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Extractor\Annotation; + +/** + * @Annotation + */ +class Translate +{ + /** + * @var string + */ + private $domain = 'messages'; + + /** + * Translate constructor. + */ + public function __construct(array $values) + { + if (isset($values['domain'])) { + $this->domain = $values['domain']; + } + } + + public function getDomain(): string + { + return $this->domain; + } +} From f740e7da7c4525f46e9b8cdc732f1b93f8b156b9 Mon Sep 17 00:00:00 2001 From: Marc Date: Wed, 11 Mar 2026 05:25:15 +0100 Subject: [PATCH 7/7] Soft deprecate annotations --- src/Annotation/Desc.php | 2 ++ src/Annotation/Ignore.php | 2 ++ src/Annotation/Translate.php | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/Annotation/Desc.php b/src/Annotation/Desc.php index 20ebbad..32771ec 100644 --- a/src/Annotation/Desc.php +++ b/src/Annotation/Desc.php @@ -12,6 +12,8 @@ namespace Translation\Extractor\Annotation; /** + * @deprecated since 2.3, this class is not used anymore. @Desc is now considered as a PHPDoc tag. + * * @Annotation */ final class Desc diff --git a/src/Annotation/Ignore.php b/src/Annotation/Ignore.php index a95a0c6..bd33625 100644 --- a/src/Annotation/Ignore.php +++ b/src/Annotation/Ignore.php @@ -12,6 +12,8 @@ namespace Translation\Extractor\Annotation; /** + * @deprecated since 2.3, this class is not used anymore. @Ignore is now considered as a PHPDoc tag. + * * @Annotation */ final class Ignore diff --git a/src/Annotation/Translate.php b/src/Annotation/Translate.php index b8673a2..20e2852 100644 --- a/src/Annotation/Translate.php +++ b/src/Annotation/Translate.php @@ -12,6 +12,8 @@ namespace Translation\Extractor\Annotation; /** + * @deprecated since 2.3, this class is not used anymore. @Translate is now considered as a PHPDoc tag. + * * @Annotation */ class Translate