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 3c9b1ef

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

19 files changed

+956
-0
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_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

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'])
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
+5Lines 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

+81Lines 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+
}
+112Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.