Skip to content

Commit 3c9b1ef

Browse files
committed
Add PhpAstExtractor
1 parent 2add2f2 commit 3c9b1ef

19 files changed

+956
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Http\Client\HttpClient;
1818
use phpDocumentor\Reflection\DocBlockFactoryInterface;
1919
use phpDocumentor\Reflection\Types\ContextFactory;
20+
use PhpParser\Parser;
2021
use PHPStan\PhpDocParser\Parser\PhpDocParser;
2122
use Psr\Cache\CacheItemPoolInterface;
2223
use Psr\Container\ContainerInterface as PsrContainerInterface;
@@ -1311,6 +1312,12 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder
13111312
$container->removeDefinition('translation.locale_switcher');
13121313
}
13131314

1315+
if (!ContainerBuilder::willBeAvailable('nikic/php-parser', Parser::class, ['symfony/translation'])) {
1316+
$container->removeDefinition('translation.extractor.php_ast');
1317+
} else {
1318+
$container->removeDefinition('translation.extractor.php');
1319+
}
1320+
13141321
$loader->load('translation_providers.php');
13151322

13161323
// Use the "real" translator instead of the identity default

src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Symfony\Component\Translation\Dumper\YamlFileDumper;
2727
use Symfony\Component\Translation\Extractor\ChainExtractor;
2828
use Symfony\Component\Translation\Extractor\ExtractorInterface;
29+
use Symfony\Component\Translation\Extractor\PhpAstExtractor;
2930
use Symfony\Component\Translation\Extractor\PhpExtractor;
3031
use Symfony\Component\Translation\Formatter\MessageFormatter;
3132
use Symfony\Component\Translation\Loader\CsvFileLoader;
@@ -151,6 +152,9 @@
151152
->set('translation.extractor.php', PhpExtractor::class)
152153
->tag('translation.extractor', ['alias' => 'php'])
153154

155+
->set('translation.extractor.php_ast', PhpAstExtractor::class)
156+
->tag('translation.extractor', ['alias' => 'php'])
157+
154158
->set('translation.reader', TranslationReader::class)
155159
->alias(TranslationReaderInterface::class, 'translation.reader')
156160

src/Symfony/Component/Translation/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
6.2
5+
---
6+
7+
* Add `PhpAstExtractor`
8+
49
6.1
510
---
611

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Translation\Extractor;
13+
14+
use PhpParser\NodeTraverser;
15+
use PhpParser\NodeVisitor;
16+
use PhpParser\Parser;
17+
use PhpParser\ParserFactory;
18+
use Symfony\Component\Finder\Finder;
19+
use Symfony\Component\Translation\Extractor\Visitor\ConstraintVisitor;
20+
use Symfony\Component\Translation\Extractor\Visitor\TranslatableMessageVisitor;
21+
use Symfony\Component\Translation\Extractor\Visitor\TransMethodVisitor;
22+
use Symfony\Component\Translation\Extractor\Visitor\Visitor;
23+
use Symfony\Component\Translation\MessageCatalogue;
24+
25+
/**
26+
* PhpAstExtractor extracts translation messages from a PHP AST.
27+
*
28+
* @author Mathieu Santostefano <msantostefano@protonmail.com>
29+
*/
30+
class PhpAstExtractor extends AbstractFileExtractor implements ExtractorInterface
31+
{
32+
private string $prefix = '';
33+
private Parser $parser;
34+
/**
35+
* @var array<Visitor&NodeVisitor>
36+
*/
37+
private array $visitors;
38+
39+
public function __construct()
40+
{
41+
$this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
42+
$this->visitors = [
43+
new TransMethodVisitor(),
44+
new TranslatableMessageVisitor(),
45+
new ConstraintVisitor(),
46+
];
47+
}
48+
49+
public function extract(iterable|string $resource, MessageCatalogue $catalogue)
50+
{
51+
foreach ($this->extractFiles($resource) as $file) {
52+
$traverser = new NodeTraverser();
53+
foreach ($this->visitors as $visitor) {
54+
$visitor->initialize($catalogue, $file, $this->prefix);
55+
$traverser->addVisitor($visitor);
56+
}
57+
58+
$nodes = $this->parser->parse(file_get_contents($file));
59+
$traverser->traverse($nodes);
60+
}
61+
}
62+
63+
public function setPrefix(string $prefix)
64+
{
65+
$this->prefix = $prefix;
66+
}
67+
68+
protected function canBeExtracted(string $file): bool
69+
{
70+
return 'php' === pathinfo($file, \PATHINFO_EXTENSION) && $this->isFile($file);
71+
}
72+
73+
protected function extractFromDirectory(array|string $resource): iterable|Finder
74+
{
75+
if (!class_exists(Finder::class)) {
76+
throw new \LogicException(sprintf('You cannot use "%s" as the "symfony/finder" package is not installed. Try running "composer require symfony/finder".', static::class));
77+
}
78+
79+
return (new Finder())->files()->name('*.php')->in($resource);
80+
}
81+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Translation\Extractor\Visitor;
13+
14+
use PhpParser\Node;
15+
use Symfony\Component\Translation\MessageCatalogue;
16+
17+
/**
18+
* @author Mathieu Santostefano <msantostefano@protonmail.com>
19+
*/
20+
abstract class AbstractVisitor implements Visitor
21+
{
22+
protected MessageCatalogue $catalogue;
23+
protected \SplFileInfo $file;
24+
protected string $messagePrefix;
25+
26+
public function initialize(MessageCatalogue $catalogue, \SplFileInfo $file, string $messagePrefix): void
27+
{
28+
$this->catalogue = $catalogue;
29+
$this->file = $file;
30+
$this->messagePrefix = $messagePrefix;
31+
}
32+
33+
protected function addMessageToCatalogue(string $message, ?string $domain, int $line): void
34+
{
35+
$domain ??= 'messages';
36+
$this->catalogue->set($message, $this->messagePrefix.$message, $domain);
37+
$metadata = $this->catalogue->getMetadata($message, $domain) ?? [];
38+
$normalizedFilename = preg_replace('{[\\\\/]+}', '/', $this->file);
39+
$metadata['sources'][] = $normalizedFilename.':'.$line;
40+
$this->catalogue->setMetadata($message, $metadata, $domain);
41+
}
42+
43+
protected function getStringArgument(Node\Expr\CallLike|Node\Attribute $node, int|string $index): ?string
44+
{
45+
$args = $node instanceof Node\Expr\CallLike ? $node->getArgs() : $node->args;
46+
47+
if (\is_string($index)) {
48+
return $this->getStringNamedArgument($node, $index);
49+
}
50+
51+
if (!\array_key_exists($index, $args)) {
52+
return null;
53+
}
54+
55+
if ('' === $result = $this->getStringValue($args[$index]->value)) {
56+
return null;
57+
}
58+
59+
return $result;
60+
}
61+
62+
protected function hasNodeNamedArguments(Node\Expr\CallLike|Node\Attribute $node): bool
63+
{
64+
$args = $node instanceof Node\Expr\CallLike ? $node->getArgs() : $node->args;
65+
66+
foreach ($args as $arg) {
67+
if (null !== $arg->name) {
68+
return true;
69+
}
70+
}
71+
72+
return false;
73+
}
74+
75+
private function getStringNamedArgument(Node\Expr\CallLike|Node\Attribute $node, string $argumentName): ?string
76+
{
77+
$args = $node instanceof Node\Expr\CallLike ? $node->getArgs() : $node->args;
78+
79+
foreach ($args as $arg) {
80+
if ($arg->name?->toString() === $argumentName) {
81+
return $this->getStringValue($arg->value);
82+
}
83+
}
84+
85+
return null;
86+
}
87+
88+
private function getStringValue(Node $node): ?string
89+
{
90+
if ($node instanceof Node\Scalar\String_) {
91+
return $node->value;
92+
}
93+
94+
if ($node instanceof Node\Expr\BinaryOp\Concat) {
95+
if (null === $left = $this->getStringValue($node->left)) {
96+
return null;
97+
}
98+
99+
if (null === $right = $this->getStringValue($node->right)) {
100+
return null;
101+
}
102+
103+
return $left.$right;
104+
}
105+
106+
if ($node instanceof Node\Expr\Assign && $node->expr instanceof Node\Scalar\String_) {
107+
return $node->expr->value;
108+
}
109+
110+
return null;
111+
}
112+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy