Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9617446

Browse filesBrowse files
committed
Add PhpAstExtractor
1 parent 2add2f2 commit 9617446
Copy full SHA for 9617446
Expand file treeCollapse file tree

19 files changed

+979
-1
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+7Lines 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');
1317+
} else {
1318+
$container->removeDefinition('translation.extractor.php_ast');
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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php
+4Lines 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_ast'])
157+
154158
->set('translation.reader', TranslationReader::class)
155159
->alias(TranslationReaderInterface::class, 'translation.reader')
156160

‎src/Symfony/Component/Translation/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Translation/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Parameters implementing `TranslatableInterface` are processed
88
* Add the file extension to the `XliffFileDumper` constructor
9+
* Add `PhpAstExtractor`
910

1011
5.4
1112
---
+83Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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\Parser;
16+
use PhpParser\ParserFactory;
17+
use Symfony\Component\Finder\Finder;
18+
use Symfony\Component\Translation\Extractor\Visitor\ConstraintVisitor;
19+
use Symfony\Component\Translation\Extractor\Visitor\TranslatableMessageVisitor;
20+
use Symfony\Component\Translation\Extractor\Visitor\TransMethodVisitor;
21+
use Symfony\Component\Translation\Extractor\Visitor\Visitor;
22+
use Symfony\Component\Translation\MessageCatalogue;
23+
24+
/**
25+
* PhpAstExtractor extracts translation messages from a PHP AST.
26+
*
27+
* @author Mathieu Santostefano <msantostefano@protonmail.com>
28+
*/
29+
class PhpAstExtractor extends AbstractFileExtractor implements ExtractorInterface
30+
{
31+
private string $prefix = '';
32+
private Parser $parser;
33+
/**
34+
* @var Visitor[]
35+
*/
36+
private array $visitors;
37+
38+
public function __construct()
39+
{
40+
$this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
41+
$this->visitors = [
42+
new TransMethodVisitor(),
43+
new TranslatableMessageVisitor(),
44+
new ConstraintVisitor(),
45+
];
46+
}
47+
48+
public function extract(iterable|string $resource, MessageCatalogue $catalogue)
49+
{
50+
$files = $this->extractFiles($resource);
51+
foreach ($files 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 $this->isFile($file) && 'php' === pathinfo($file, \PATHINFO_EXTENSION);
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+
$finder = new Finder();
80+
81+
return $finder->files()->name('*.php')->in($resource);
82+
}
83+
}
+115Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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
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+
if (null === $domain) {
36+
$domain = 'messages';
37+
}
38+
39+
$this->catalogue->set($message, $this->messagePrefix.$message, $domain);
40+
$metadata = $this->catalogue->getMetadata($message, $domain) ?? [];
41+
$normalizedFilename = preg_replace('{[\\\\/]+}', '/', $this->file);
42+
$metadata['sources'][] = $normalizedFilename.':'.$line;
43+
$this->catalogue->setMetadata($message, $metadata, $domain);
44+
}
45+
46+
protected function getStringArgument(Node\Expr\CallLike|Node\Attribute $node, int|string $index): ?string
47+
{
48+
if (\is_string($index)) {
49+
return $this->getStringNamedArgument($node, $index);
50+
}
51+
52+
if (!\array_key_exists($index, $node->args)) {
53+
return null;
54+
}
55+
56+
$result = $this->getStringValue($node->args[$index]->value);
57+
58+
if ('' === $result) {
59+
return null;
60+
}
61+
62+
return $result;
63+
}
64+
65+
protected function hasNodeNamedArguments(Node\Expr\CallLike|Node\Attribute $node): bool
66+
{
67+
foreach ($node->args as $arg) {
68+
if (null !== $arg->name) {
69+
return true;
70+
}
71+
}
72+
73+
return false;
74+
}
75+
76+
private function getStringNamedArgument(Node\Expr\CallLike|Node\Attribute $node, string $argumentName): ?string
77+
{
78+
foreach ($node->args as $arg) {
79+
if ($arg->name?->toString() === $argumentName) {
80+
return $this->getStringValue($arg->value);
81+
}
82+
}
83+
84+
return null;
85+
}
86+
87+
private function getStringValue(Node $node): ?string
88+
{
89+
if ($node instanceof Node\Scalar\String_) {
90+
return $node->value;
91+
}
92+
93+
if ($node instanceof Node\Expr\BinaryOp\Concat) {
94+
$left = $this->getStringValue($node->left);
95+
if (null === $left) {
96+
return null;
97+
}
98+
99+
$right = $this->getStringValue($node->right);
100+
if (null === $right) {
101+
return null;
102+
}
103+
104+
return $left.$right;
105+
}
106+
107+
if ($node instanceof Node\Expr\Assign) {
108+
if ($node->expr instanceof Node\Scalar\String_) {
109+
return $node->expr->value;
110+
}
111+
}
112+
113+
return null;
114+
}
115+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.