1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12use Twig\Environment;
13use Twig\Node\Node;
14use Twig\Node\SetNode;
15use Twig\Node\TextNode;
16use Twig\Parser;
17use Twig\Source;
18use Twig\Token;
19use Twig\TokenParser\AbstractTokenParser;
20use Twig\TokenStream;
21
22class Twig_Tests_ParserTest extends \PHPUnit\Framework\TestCase
23{
24    /**
25     * @expectedException \Twig\Error\SyntaxError
26     */
27    public function testSetMacroThrowsExceptionOnReservedMethods()
28    {
29        $parser = $this->getParser();
30        $parser->setMacro('parent', $this->getMockBuilder('\Twig\Node\MacroNode')->disableOriginalConstructor()->getMock());
31    }
32
33    /**
34     * @expectedException        \Twig\Error\SyntaxError
35     * @expectedExceptionMessage Unknown "foo" tag. Did you mean "for" at line 1?
36     */
37    public function testUnknownTag()
38    {
39        $stream = new TokenStream([
40            new Token(Token::BLOCK_START_TYPE, '', 1),
41            new Token(Token::NAME_TYPE, 'foo', 1),
42            new Token(Token::BLOCK_END_TYPE, '', 1),
43            new Token(Token::EOF_TYPE, '', 1),
44        ]);
45        $parser = new Parser(new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock()));
46        $parser->parse($stream);
47    }
48
49    /**
50     * @expectedException        \Twig\Error\SyntaxError
51     * @expectedExceptionMessage Unknown "foobar" tag at line 1.
52     */
53    public function testUnknownTagWithoutSuggestions()
54    {
55        $stream = new TokenStream([
56            new Token(Token::BLOCK_START_TYPE, '', 1),
57            new Token(Token::NAME_TYPE, 'foobar', 1),
58            new Token(Token::BLOCK_END_TYPE, '', 1),
59            new Token(Token::EOF_TYPE, '', 1),
60        ]);
61        $parser = new Parser(new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock()));
62        $parser->parse($stream);
63    }
64
65    /**
66     * @dataProvider getFilterBodyNodesData
67     */
68    public function testFilterBodyNodes($input, $expected)
69    {
70        $parser = $this->getParser();
71
72        $this->assertEquals($expected, $parser->filterBodyNodes($input));
73    }
74
75    public function getFilterBodyNodesData()
76    {
77        return [
78            [
79                new Node([new TextNode('   ', 1)]),
80                new Node([]),
81            ],
82            [
83                $input = new Node([new SetNode(false, new Node(), new Node(), 1)]),
84                $input,
85            ],
86            [
87                $input = new Node([new SetNode(true, new Node(), new Node([new Node([new TextNode('foo', 1)])]), 1)]),
88                $input,
89            ],
90        ];
91    }
92
93    /**
94     * @dataProvider getFilterBodyNodesDataThrowsException
95     * @expectedException \Twig\Error\SyntaxError
96     */
97    public function testFilterBodyNodesThrowsException($input)
98    {
99        $parser = $this->getParser();
100
101        $parser->filterBodyNodes($input);
102    }
103
104    public function getFilterBodyNodesDataThrowsException()
105    {
106        return [
107            [new TextNode('foo', 1)],
108            [new Node([new Node([new TextNode('foo', 1)])])],
109        ];
110    }
111
112    /**
113     * @dataProvider getFilterBodyNodesWithBOMData
114     */
115    public function testFilterBodyNodesWithBOM($emptyNode)
116    {
117        $this->assertNull($this->getParser()->filterBodyNodes(new TextNode(\chr(0xEF).\chr(0xBB).\chr(0xBF).$emptyNode, 1)));
118    }
119
120    public function getFilterBodyNodesWithBOMData()
121    {
122        return [
123            [' '],
124            ["\t"],
125            ["\n"],
126            ["\n\t\n   "],
127        ];
128    }
129
130    public function testParseIsReentrant()
131    {
132        $twig = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), [
133            'autoescape' => false,
134            'optimizations' => 0,
135        ]);
136        $twig->addTokenParser(new TestTokenParser());
137
138        $parser = new Parser($twig);
139
140        $parser->parse(new TokenStream([
141            new Token(Token::BLOCK_START_TYPE, '', 1),
142            new Token(Token::NAME_TYPE, 'test', 1),
143            new Token(Token::BLOCK_END_TYPE, '', 1),
144            new Token(Token::VAR_START_TYPE, '', 1),
145            new Token(Token::NAME_TYPE, 'foo', 1),
146            new Token(Token::VAR_END_TYPE, '', 1),
147            new Token(Token::EOF_TYPE, '', 1),
148        ]));
149
150        $this->assertNull($parser->getParent());
151    }
152
153    public function testGetVarName()
154    {
155        $twig = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), [
156            'autoescape' => false,
157            'optimizations' => 0,
158        ]);
159
160        $twig->parse($twig->tokenize(new Source(<<<EOF
161{% from _self import foo %}
162
163{% macro foo() %}
164    {{ foo }}
165{% endmacro %}
166EOF
167        , 'index')));
168
169        // The getVarName() must not depend on the template loaders,
170        // If this test does not throw any exception, that's good.
171        // see https://github.com/symfony/symfony/issues/4218
172        $this->addToAssertionCount(1);
173    }
174
175    protected function getParser()
176    {
177        $parser = new TestParser(new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock()));
178        $parser->setParent(new Node());
179        $parser->stream = new TokenStream([]);
180
181        return $parser;
182    }
183}
184
185class TestParser extends Parser
186{
187    public $stream;
188
189    public function filterBodyNodes(Twig_NodeInterface $node)
190    {
191        return parent::filterBodyNodes($node);
192    }
193}
194
195class TestTokenParser extends AbstractTokenParser
196{
197    public function parse(Token $token)
198    {
199        // simulate the parsing of another template right in the middle of the parsing of the current template
200        $this->parser->parse(new TokenStream([
201            new Token(Token::BLOCK_START_TYPE, '', 1),
202            new Token(Token::NAME_TYPE, 'extends', 1),
203            new Token(Token::STRING_TYPE, 'base', 1),
204            new Token(Token::BLOCK_END_TYPE, '', 1),
205            new Token(Token::EOF_TYPE, '', 1),
206        ]));
207
208        $this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
209
210        return new Node([]);
211    }
212
213    public function getTag()
214    {
215        return 'test';
216    }
217}
218