Passed
Push — v5 ( c5e113...41d382 )
by Andrew
17:29 queued 14s
created

TemplateCommentsParser::hasInheritance()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 2
eloc 1
c 1
b 1
f 0
nc 2
nop 0
dl 0
loc 3
rs 10
1
<?php
2
/**
3
 * Template Comments plugin for Craft CMS
4
 *
5
 * Adds a HTML comment to demarcate each Twig template that is included or extended.
6
 *
7
 * @link      https://nystudio107.com/
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @copyright tag
Loading history...
8
 * @copyright Copyright (c)  nystudio107
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
9
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @package tag in file comment
Loading history...
Coding Style introduced by
Missing @author tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
10
11
/**
12
 * This is a wholesale copy pasta of the Twig Parser class; with one modification so that it
13
 * does not throw a "A block definition cannot be nested under non-capturing nodes" SyntaxError exception
14
 *
15
 * This exception is explained in detail here: https://github.com/twigphp/Twig/issues/3926
16
 *
17
 * So that Template Comments can add HTML comments that indicate any {% include %} or {% extends %} templates
18
 * with execution timing, it uses its own CommentTemplateLoader which wraps the loaded template
19
 * with {% comments 'templateName' %}<loaded-template>{% endcomments %}
20
 *
21
 * There is a corresponding CommentsTokenParser that takes care of parsing the {% comments %} tags
22
 *
23
 * This worked in Twig 1.x and 2.x, but they added a check for block definitions nested under non-capturing
24
 * blocks in Twig 3.x, which causes it to throw an exception in that case. So if you end up with something like:
25
 *
26
 * {% comments 'index' %}
27
 *     {% block conent %}
28
 *     {% endblock %}
29
 * {% encomments %}
30
 *
31
 * ...the SyntaxError exception will be thrown.
32
 *
33
 * I tried adding implements NodeCaptureInterface to the CommentsNode but that resulted in the Parser returning
34
 * the node, causing duplicate rendering for every {% include %} or {% extends %} that was wrapped in a
35
 * {% comments %} tag
36
 *
37
 * We can't just subclass the Parser class, because the properties and methods are private
38
 *
39
 * So here we are.
40
 *
41
 * Don't judge me
42
 */
43
44
namespace nystudio107\templatecomments\web\twig;
45
46
use nystudio107\templatecomments\helpers\Reflection as ReflectionHelper;
47
use ReflectionException;
48
use Twig\Environment;
49
use Twig\Error\SyntaxError;
50
use Twig\ExpressionParser;
0 ignored issues
show
Bug introduced by
The type Twig\ExpressionParser was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
51
use Twig\Node\BlockNode;
52
use Twig\Node\BlockReferenceNode;
53
use Twig\Node\BodyNode;
54
use Twig\Node\Expression\AbstractExpression;
55
use Twig\Node\MacroNode;
56
use Twig\Node\ModuleNode;
57
use Twig\Node\Node;
58
use Twig\Node\NodeCaptureInterface;
59
use Twig\Node\NodeOutputInterface;
60
use Twig\Node\PrintNode;
61
use Twig\Node\TextNode;
62
use Twig\NodeTraverser;
63
use Twig\Parser;
64
use Twig\Token;
65
use Twig\TokenParser\TokenParserInterface;
66
use Twig\TokenStream;
67
use function chr;
68
use function count;
69
use function get_class;
70
use function is_array;
71
72
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
73
 * @author Fabien Potencier <[email protected]>
74
 */
0 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @package tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
75
class TemplateCommentsParser extends Parser
76
{
77
    private $stack = [];
0 ignored issues
show
Coding Style introduced by
Private member variable "stack" must be prefixed with an underscore
Loading history...
78
    private $stream;
0 ignored issues
show
Coding Style introduced by
Private member variable "stream" must be prefixed with an underscore
Loading history...
79
    private $parent;
0 ignored issues
show
Coding Style introduced by
Private member variable "parent" must be prefixed with an underscore
Loading history...
80
    private $visitors;
0 ignored issues
show
Coding Style introduced by
Private member variable "visitors" must be prefixed with an underscore
Loading history...
81
    private $expressionParser;
0 ignored issues
show
Coding Style introduced by
Private member variable "expressionParser" must be prefixed with an underscore
Loading history...
82
    private $blocks;
0 ignored issues
show
Coding Style introduced by
Private member variable "blocks" must be prefixed with an underscore
Loading history...
83
    private $blockStack;
0 ignored issues
show
Coding Style introduced by
Private member variable "blockStack" must be prefixed with an underscore
Loading history...
84
    private $macros;
0 ignored issues
show
Coding Style introduced by
Private member variable "macros" must be prefixed with an underscore
Loading history...
85
    private $env;
0 ignored issues
show
Coding Style introduced by
Private member variable "env" must be prefixed with an underscore
Loading history...
86
    private $importedSymbols;
0 ignored issues
show
Coding Style introduced by
Private member variable "importedSymbols" must be prefixed with an underscore
Loading history...
87
    private $traits;
0 ignored issues
show
Coding Style introduced by
Private member variable "traits" must be prefixed with an underscore
Loading history...
88
    private $embeddedTemplates = [];
0 ignored issues
show
Coding Style introduced by
Private member variable "embeddedTemplates" must be prefixed with an underscore
Loading history...
89
    private $varNameSalt = 0;
0 ignored issues
show
Coding Style introduced by
Private member variable "varNameSalt" must be prefixed with an underscore
Loading history...
90
    private $expressionParserClass;
0 ignored issues
show
Coding Style introduced by
Private member variable "expressionParserClass" must be prefixed with an underscore
Loading history...
91
92
    public function __construct(Environment $env)
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function __construct()
Loading history...
93
    {
94
        $this->env = $env;
95
        $this->expressionParserClass = ExpressionParser::class;
96
        // Get the existing parser object used by the Twig $env
97
        try {
98
            $parserReflection = ReflectionHelper::getReflectionProperty($env, 'parser');
99
        } catch (ReflectionException $e) {
100
            return;
101
        }
102
        $parserReflection->setAccessible(true);
103
        $parser = $parserReflection->getValue($env);
104
        if ($parser === null) {
105
            return;
106
        }
107
        // Get the expression parser used by the current parser
108
        try {
109
            $expressionParserReflection = ReflectionHelper::getReflectionProperty($parser, 'expressionParser');
110
        } catch (ReflectionException $e) {
111
            return;
112
        }
113
        // Preserve the existing expression parser and use it
114
        $expressionParserReflection->setAccessible(true);
115
        $expressionParser = $expressionParserReflection->getValue($parser);
116
        $this->expressionParserClass = get_class($expressionParser);
117
    }
118
119
    public function getVarName(): string
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function getVarName()
Loading history...
120
    {
121
        return sprintf('__internal_parse_%d', $this->varNameSalt++);
122
    }
123
124
    public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function parse()
Loading history...
125
    {
126
        $vars = get_object_vars($this);
127
        unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames'], $vars['varNameSalt']);
128
        $this->stack[] = $vars;
129
130
        // node visitors
131
        if (null === $this->visitors) {
132
            $this->visitors = $this->env->getNodeVisitors();
133
        }
134
135
        if (null === $this->expressionParser) {
136
            $this->expressionParser = new $this->expressionParserClass($this, $this->env);
137
        }
138
139
        $this->stream = $stream;
140
        $this->parent = null;
141
        $this->blocks = [];
142
        $this->macros = [];
143
        $this->traits = [];
144
        $this->blockStack = [];
145
        $this->importedSymbols = [[]];
146
        $this->embeddedTemplates = [];
147
148
        try {
149
            $body = $this->subparse($test, $dropNeedle);
150
151
            if (null !== $this->parent && null === $body = $this->filterBodyNodes($body)) {
152
                $body = new Node();
153
            }
154
        } catch (SyntaxError $e) {
155
            if (!$e->getSourceContext()) {
156
                $e->setSourceContext($this->stream->getSourceContext());
157
            }
158
159
            if (!$e->getTemplateLine()) {
160
                $e->setTemplateLine($this->stream->getCurrent()->getLine());
161
            }
162
163
            throw $e;
164
        }
165
166
        $node = new ModuleNode(new BodyNode([$body]), $this->parent, new Node($this->blocks), new Node($this->macros), new Node($this->traits), $this->embeddedTemplates, $stream->getSourceContext());
167
168
        $traverser = new NodeTraverser($this->env, $this->visitors);
169
170
        /** @var ModuleNode $node */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
171
        $node = $traverser->traverse($node);
172
173
        // restore previous stack so previous parse() call can resume working
174
        foreach (array_pop($this->stack) as $key => $val) {
175
            $this->$key = $val;
176
        }
177
178
        return $node;
179
    }
180
181
    public function subparse($test, bool $dropNeedle = false): Node
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function subparse()
Loading history...
182
    {
183
        $lineno = $this->getCurrentToken()->getLine();
184
        $rv = [];
185
        while (!$this->stream->isEOF()) {
186
            switch ($this->getCurrentToken()->getType()) {
187
                case /* Token::TEXT_TYPE */ 0:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
188
                    $token = $this->stream->next();
189
                    $rv[] = new TextNode($token->getValue(), $token->getLine());
190
                    break;
191
192
                case /* Token::VAR_START_TYPE */ 2:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
193
                    $token = $this->stream->next();
194
                    $expr = $this->expressionParser->parseExpression();
195
                    $this->stream->expect(/* Token::VAR_END_TYPE */ 4);
196
                    $rv[] = new PrintNode($expr, $token->getLine());
197
                    break;
198
199
                case /* Token::BLOCK_START_TYPE */ 1:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
200
                    $this->stream->next();
201
                    $token = $this->getCurrentToken();
202
203
                    if (/* Token::NAME_TYPE */ 5 !== $token->getType()) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
204
                        throw new SyntaxError('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext());
205
                    }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
206
207
                    if (null !== $test && $test($token)) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
208
                        if ($dropNeedle) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
209
                            $this->stream->next();
210
                        }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
211
212
                        if (1 === count($rv)) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
213
                            return $rv[0];
214
                        }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
215
216
                        return new Node($rv, [], $lineno);
217
                    }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
218
219
                    if (!$subparser = $this->env->getTokenParser($token->getValue())) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
220
                        if (null !== $test) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
221
                            $e = new SyntaxError(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
222
223
                            if (is_array($test) && isset($test[0]) && $test[0] instanceof TokenParserInterface) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
224
                                $e->appendMessage(sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno));
225
                            }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
226
                        } else {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
227
                            $e = new SyntaxError(sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
228
                            $e->addSuggestions($token->getValue(), array_keys($this->env->getTokenParsers()));
229
                        }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
230
231
                        throw $e;
232
                    }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
233
234
                    $this->stream->next();
235
236
                    $subparser->setParser($this);
237
                    $node = $subparser->parse($token);
238
                    if (null !== $node) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
239
                        $rv[] = $node;
240
                    }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
241
                    break;
242
243
                default:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
244
                    throw new SyntaxError('Lexer or parser ended up in unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext());
245
            }
246
        }
247
248
        if (1 === count($rv)) {
249
            return $rv[0];
250
        }
251
252
        return new Node($rv, [], $lineno);
253
    }
254
255
    public function getBlockStack(): array
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function getBlockStack()
Loading history...
256
    {
257
        return $this->blockStack;
258
    }
259
260
    public function peekBlockStack()
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function peekBlockStack()
Loading history...
261
    {
262
        return $this->blockStack[count($this->blockStack) - 1] ?? null;
263
    }
264
265
    public function popBlockStack(): void
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function popBlockStack()
Loading history...
266
    {
267
        array_pop($this->blockStack);
268
    }
269
270
    public function pushBlockStack($name): void
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function pushBlockStack()
Loading history...
271
    {
272
        $this->blockStack[] = $name;
273
    }
274
275
    public function hasBlock(string $name): bool
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function hasBlock()
Loading history...
276
    {
277
        return isset($this->blocks[$name]);
278
    }
279
280
    public function getBlock(string $name): Node
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function getBlock()
Loading history...
281
    {
282
        return $this->blocks[$name];
283
    }
284
285
    public function setBlock(string $name, BlockNode $value): void
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function setBlock()
Loading history...
286
    {
287
        $this->blocks[$name] = new BodyNode([$value], [], $value->getTemplateLine());
288
    }
289
290
    public function hasMacro(string $name): bool
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function hasMacro()
Loading history...
291
    {
292
        return isset($this->macros[$name]);
293
    }
294
295
    public function setMacro(string $name, MacroNode $node): void
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function setMacro()
Loading history...
296
    {
297
        $this->macros[$name] = $node;
298
    }
299
300
    public function addTrait($trait): void
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function addTrait()
Loading history...
301
    {
302
        $this->traits[] = $trait;
303
    }
304
305
    public function hasTraits(): bool
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function hasTraits()
Loading history...
306
    {
307
        return count($this->traits) > 0;
308
    }
309
310
    public function embedTemplate(ModuleNode $template)
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function embedTemplate()
Loading history...
311
    {
312
        $template->setIndex(mt_rand());
313
314
        $this->embeddedTemplates[] = $template;
315
    }
316
317
    public function addImportedSymbol(string $type, string $alias, string $name = null, AbstractExpression $node = null): void
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function addImportedSymbol()
Loading history...
318
    {
319
        $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $node];
320
    }
321
322
    public function getImportedSymbol(string $type, string $alias)
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function getImportedSymbol()
Loading history...
323
    {
324
        // if the symbol does not exist in the current scope (0), try in the main/global scope (last index)
325
        return $this->importedSymbols[0][$type][$alias] ?? ($this->importedSymbols[count($this->importedSymbols) - 1][$type][$alias] ?? null);
326
    }
327
328
    public function isMainScope(): bool
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function isMainScope()
Loading history...
329
    {
330
        return 1 === count($this->importedSymbols);
331
    }
332
333
    public function pushLocalScope(): void
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function pushLocalScope()
Loading history...
334
    {
335
        array_unshift($this->importedSymbols, []);
336
    }
337
338
    public function popLocalScope(): void
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function popLocalScope()
Loading history...
339
    {
340
        array_shift($this->importedSymbols);
341
    }
342
343
    public function getExpressionParser(): ExpressionParser
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function getExpressionParser()
Loading history...
344
    {
345
        return $this->expressionParser;
346
    }
347
348
    public function getParent(): ?Node
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function getParent()
Loading history...
349
    {
350
        return $this->parent;
351
    }
352
353
    public function setParent(?Node $parent): void
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function setParent()
Loading history...
354
    {
355
        $this->parent = $parent;
356
    }
357
358
    public function hasInheritance()
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function hasInheritance()
Loading history...
359
    {
360
        return $this->parent || 0 < count($this->traits);
361
    }
362
363
    public function getStream(): TokenStream
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function getStream()
Loading history...
364
    {
365
        return $this->stream;
366
    }
367
368
    public function getCurrentToken(): Token
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function getCurrentToken()
Loading history...
369
    {
370
        return $this->stream->getCurrent();
371
    }
372
373
    private function filterBodyNodes(Node $node, bool $nested = false): ?Node
0 ignored issues
show
Coding Style introduced by
Private method name "TemplateCommentsParser::filterBodyNodes" must be prefixed with an underscore
Loading history...
Coding Style introduced by
Missing doc comment for function filterBodyNodes()
Loading history...
374
    {
375
        // check that the body does not contain non-empty output nodes
376
        if (
0 ignored issues
show
Coding Style introduced by
First condition of a multi-line IF statement must directly follow the opening parenthesis
Loading history...
377
            ($node instanceof TextNode && !ctype_space($node->getAttribute('data')))
0 ignored issues
show
Coding Style introduced by
Each line in a multi-line IF statement must begin with a boolean operator
Loading history...
378
            || (!$node instanceof TextNode && !$node instanceof BlockReferenceNode && $node instanceof NodeOutputInterface)
379
        ) {
380
            if (str_contains((string)$node, chr(0xEF) . chr(0xBB) . chr(0xBF))) {
381
                $t = substr($node->getAttribute('data'), 3);
382
                if ('' === $t || ctype_space($t)) {
383
                    // bypass empty nodes starting with a BOM
384
                    return null;
385
                }
386
            }
387
388
            throw new SyntaxError('A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag?', $node->getTemplateLine(), $this->stream->getSourceContext());
389
        }
390
391
        // bypass nodes that "capture" the output
392
        if ($node instanceof NodeCaptureInterface) {
393
            // a "block" tag in such a node will serve as a block definition AND be displayed in place as well
394
            return $node;
395
        }
396
397
        /**
398
         * We intentionally skip this check to avoid throwing an exception, so our {% comments %} tag can
399
         * render correctly
400
         * // "block" tags that are not captured (see above) are only used for defining
401
         * // the content of the block. In such a case, nesting it does not work as
402
         * // expected as the definition is not part of the default template code flow.
403
         * if ($nested && $node instanceof BlockReferenceNode) {
404
         * throw new SyntaxError('A block definition cannot be nested under non-capturing nodes.', $node->getTemplateLine(), $this->stream->getSourceContext());
405
         * }
406
         */
407
        if ($node instanceof NodeOutputInterface) {
408
            return null;
409
        }
410
411
        // here, $nested means "being at the root level of a child template"
412
        // we need to discard the wrapping "Node" for the "body" node
413
        $nested = $nested || Node::class !== get_class($node);
414
        foreach ($node as $k => $n) {
415
            if (null !== $n && null === $this->filterBodyNodes($n, $nested)) {
416
                $node->removeNode($k);
417
            }
418
        }
419
420
        return $node;
421
    }
422
}
423