Completed
Push — master ( e25986...ffddd1 )
by Craig
07:06
created

ZikulaPhpFileExtractor::beforeTraverse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Zikula package.
5
 *
6
 * Copyright Zikula Foundation - http://zikula.org/
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 Zikula\Bundle\CoreBundle\Translation;
13
14
use Doctrine\Common\Annotations\DocParser;
15
use JMS\TranslationBundle\Annotation\Desc;
16
use JMS\TranslationBundle\Annotation\Ignore;
17
use JMS\TranslationBundle\Annotation\Meaning;
18
use JMS\TranslationBundle\Exception\RuntimeException;
19
use JMS\TranslationBundle\Logger\LoggerAwareInterface;
20
use JMS\TranslationBundle\Model\FileSource;
21
use JMS\TranslationBundle\Model\Message;
22
use JMS\TranslationBundle\Model\MessageCatalogue;
23
use JMS\TranslationBundle\Translation\Extractor\FileVisitorInterface;
24
use PhpParser\Node;
25
use PhpParser\Node\Stmt\Namespace_;
26
use PhpParser\Node\Expr\MethodCall;
27
use PhpParser\Node\Scalar\String_;
28
use PhpParser\NodeTraverser;
29
use PhpParser\NodeVisitor;
30
use Psr\Log\LoggerInterface;
31
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
32
use Zikula\Bundle\CoreBundle\HttpKernel\ZikulaHttpKernelInterface;
33
use Zikula\Core\AbstractBundle;
34
35
/**
36
 * This parser can extract translation information from PHP files.
37
 *
38
 * It parses all zikula-style translation calls.
39
 */
40
class ZikulaPhpFileExtractor implements LoggerAwareInterface, FileVisitorInterface, NodeVisitor
41
{
42
    /**
43
     * @var string
44
     */
45
    private $domain = '';
46
47
    /**
48
     * @var NodeTraverser
49
     */
50
    private $traverser;
51
52
    /**
53
     * @var MessageCatalogue
54
     */
55
    private $catalogue;
56
57
    /**
58
     * @var \SplFileInfo
59
     */
60
    private $file;
61
62
    /**
63
     * @var DocParser
64
     */
65
    private $docParser;
66
67
    /**
68
     * @var LoggerInterface
69
     */
70
    private $logger;
71
72
    /**
73
     * @var Node
74
     */
75
    private $previousNode;
76
77
    /**
78
     * @var array
79
     */
80
    private $bundles;
81
82
    /**
83
     * @var ZikulaHttpKernelInterface
84
     */
85
    private $kernel;
86
87
    /**
88
     * Possible Zikula-style translation method names
89
     *
90
     * @var array
91
     */
92
    private $methodNames = [
93
        1 => '__',
94
        2 => '__f',
95
        3 => '_n',
96
        4 => '_fn'
97
    ];
98
99
    /**
100
     * ZikulaPhpFileExtractor constructor.
101
     * @param DocParser $docParser
102
     * @param ZikulaHttpKernelInterface $kernel
103
     */
104
    public function __construct(DocParser $docParser, ZikulaHttpKernelInterface $kernel)
105
    {
106
        $this->docParser = $docParser;
107
        $this->kernel = $kernel;
108
        $bundles = $kernel->getBundles();
109
        foreach ($bundles as $bundle) {
110
            $this->bundles[$bundle->getNamespace()] = $bundle->getName();
111
        }
112
113
        $this->traverser = new NodeTraverser();
114
        $this->traverser->addVisitor($this);
115
    }
116
117
    /**
118
     * @param LoggerInterface $logger
119
     */
120
    public function setLogger(LoggerInterface $logger)
121
    {
122
        $this->logger = $logger;
123
    }
124
125
    public function enterNode(Node $node)
126
    {
127
        /**
128
         * determine domain from namespace of files.
129
         * Finder appears to start with root level files so Namespace is correct for remaining files
130
         */
131
        if ($node instanceof Namespace_) {
1 ignored issue
show
Bug introduced by
The class PhpParser\Node\Stmt\Namespace_ does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
132
            if (isset($node->name)) {
133
                $bundle = $this->getBundleFromNodeNamespace($node->name->toString());
134
                if (isset($bundle) && $bundle instanceof AbstractBundle) {
135
                    $this->domain = $bundle->getTranslationDomain();
136
                } else {
137
                    $this->domain = 'zikula';
138
                }
139
140
                return;
141
            } else {
142
                foreach ($node->stmts as $node) {
143
                    $this->enterNode($node);
144
                }
145
146
                return;
147
            }
148
        }
149
        if (!$node instanceof MethodCall
1 ignored issue
show
Bug introduced by
The class PhpParser\Node\Expr\MethodCall does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
150
            || !is_string($node->name)
151
            || !in_array(strtolower($node->name), $this->methodNames)
152
        ) {
153
            $this->previousNode = $node;
154
155
            return;
156
        }
157
158
        $ignore = false;
159
        $desc = $meaning = null;
160
        if (null !== $docComment = $this->getDocCommentForNode($node)) {
161
            foreach ($this->docParser->parse($docComment, 'file ' . $this->file . ' near line ' . $node->getLine()) as $annot) {
162
                if ($annot instanceof Ignore) {
1 ignored issue
show
Bug introduced by
The class JMS\TranslationBundle\Annotation\Ignore does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
163
                    $ignore = true;
164
                } elseif ($annot instanceof Desc) {
1 ignored issue
show
Bug introduced by
The class JMS\TranslationBundle\Annotation\Desc does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
165
                    $desc = $annot->text;
166
                } elseif ($annot instanceof Meaning) {
1 ignored issue
show
Bug introduced by
The class JMS\TranslationBundle\Annotation\Meaning does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
167
                    $meaning = $annot->text;
168
                }
169
            }
170
        }
171
172 View Code Duplication
        if (!$node->args[0]->value instanceof String_) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
Bug introduced by
The class PhpParser\Node\Scalar\String_ does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
173
            if ($ignore) {
174
                return;
175
            }
176
177
            $message = sprintf('Can only extract the translation id from a scalar string, but got "%s". Please refactor your code to make it extractable, or add the doc comment /** @Ignore */ to this code element (in %s on line %d).', get_class($node->args[0]->value), $this->file, $node->args[0]->value->getLine());
178
179
            if ($this->logger) {
180
                $this->logger->error($message);
181
182
                return;
183
            }
184
185
            throw new RuntimeException($message);
186
        }
187
188
        $id = $node->args[0]->value->value;
189
        if (in_array(strtolower($node->name), ['_n', '_fn'], true)) {
190
            // concatenate pluralized strings from zikula functions
191
            $id = $node->args[0]->value->value . '|' . $node->args[1]->value->value;
192
        }
193
194
        // determine location of domain
195
        $domainIndex = array_search(strtolower($node->name), $this->methodNames);
196
197
        if (isset($node->args[$domainIndex])) {
198 View Code Duplication
            if (!$node->args[$domainIndex]->value instanceof String_) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
Bug introduced by
The class PhpParser\Node\Scalar\String_ does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
199
                if ($ignore) {
200
                    return;
201
                }
202
203
                $message = sprintf('Can only extract the translation domain from a scalar string, but got "%s". Please refactor your code to make it extractable, or add the doc comment /** @Ignore */ to this code element (in %s on line %d).', get_class($node->args[0]->value), $this->file, $node->args[0]->value->getLine());
204
205
                if ($this->logger) {
206
                    $this->logger->error($message);
207
208
                    return;
209
                }
210
211
                throw new RuntimeException($message);
212
            }
213
214
            $domain = $node->args[$domainIndex]->value->value;
215
        } else {
216
            $domain = !empty($this->domain) ? $this->domain : 'zikula';
217
        }
218
219
        $message = new Message($id, $domain);
220
        $message->setDesc($desc);
221
        $message->setMeaning($meaning);
222
        $message->addSource(new FileSource((string)$this->file, $node->getLine()));
223
224
        $this->catalogue->add($message);
225
    }
226
227
    public function visitPhpFile(\SplFileInfo $file, MessageCatalogue $catalogue, array $ast)
228
    {
229
        $this->file = $file;
230
        $this->catalogue = $catalogue;
231
        $this->traverser->traverse($ast);
232
    }
233
234
    public function beforeTraverse(array $nodes)
235
    {
236
    }
237
238
    public function leaveNode(Node $node)
239
    {
240
    }
241
242
    public function afterTraverse(array $nodes)
243
    {
244
    }
245
246
    public function visitFile(\SplFileInfo $file, MessageCatalogue $catalogue)
247
    {
248
    }
249
250
    public function visitTwigFile(\SplFileInfo $file, MessageCatalogue $catalogue, \Twig_Node $ast)
251
    {
252
    }
253
254
    private function getDocCommentForNode(Node $node)
255
    {
256
        // check if there is a doc comment for the ID argument
257
        // ->trans(/** @Desc("FOO") */ 'my.id')
258
        if (null !== $comment = $node->args[0]->getDocComment()) {
259
            return $comment->getText();
260
        }
261
262
        // this may be placed somewhere up in the hierarchy,
263
        // -> /** @Desc("FOO") */ trans('my.id')
264
        // /** @Desc("FOO") */ ->trans('my.id')
265
        // /** @Desc("FOO") */ $translator->trans('my.id')
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
266
        if (null !== $comment = $node->getDocComment()) {
267
            return $comment->getText();
268
        } elseif (null !== $this->previousNode && $this->previousNode->getDocComment() !== null) {
269
            $comment = $this->previousNode->getDocComment();
270
271
            return is_object($comment) ? $comment->getText() : $comment;
272
        }
273
274
        return null;
275
    }
276
277
    /**
278
     * Search namespaces for match and return BundleObject
279
     * @param $nodeNamespace
280
     * @return BundleInterface|null
281
     */
282
    private function getBundleFromNodeNamespace($nodeNamespace)
283
    {
284
        foreach ($this->bundles as $namespace => $bundleName) {
285
            if (false !== strpos($nodeNamespace, $namespace)) {
286
                if ($this->kernel->isBundle($bundleName)) {
287
                    return $this->kernel->getBundle($bundleName);
288
                }
289
            }
290
        }
291
292
        return null;
293
    }
294
}
295