• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / latte / 16317493748

16 Jul 2025 10:49AM UTC coverage: 93.696% (-0.002%) from 93.698%
16317493748

push

github

dg
PrintContent: refactoring and updated operator precedence and associativity table

23 of 23 new or added lines in 1 file covered. (100.0%)

30 existing lines in 15 files now uncovered.

5187 of 5536 relevant lines covered (93.7%)

0.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.39
/src/Latte/Compiler/TagLexer.php
1
<?php
2

3
/**
4
 * This file is part of the Latte (https://latte.nette.org)
5
 * Copyright (c) 2008 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Latte\Compiler;
11

12
use Latte\CompileException;
13
use function array_splice, constant, count, is_float, is_int, is_numeric, ord, preg_last_error, preg_last_error_msg, preg_match, preg_match_all, str_contains, str_replace, str_split, strlen, strtolower, substr, trim;
14
use const PREG_SET_ORDER, PREG_UNMATCHED_AS_NULL;
15

16

17
/**
18
 * Lexer for PHP-like expression language used in tags.
19
 */
20
final class TagLexer
21
{
22
        private const Keywords = [
23
                'and' => Token::Php_LogicalAnd,
24
                'array' => Token::Php_Array,
25
                'clone' => Token::Php_Clone,
26
                'default' => Token::Php_Default,
27
                'in' => Token::Php_In,
28
                'instanceof' => Token::Php_Instanceof,
29
                'new' => Token::Php_New,
30
                'or' => Token::Php_LogicalOr,
31
                'return' => Token::Php_Return,
32
                'xor' => Token::Php_LogicalXor,
33
                'null' => Token::Php_Null,
34
                'true' => Token::Php_True,
35
                'false' => Token::Php_False,
36
        ];
37

38
        private const KeywordsFollowed = [ // must follows ( & =
39
                'empty' => Token::Php_Empty,
40
                'fn' => Token::Php_Fn,
41
                'function' => Token::Php_Function,
42
                'isset' => Token::Php_Isset,
43
                'list' => Token::Php_List,
44
                'match' => Token::Php_Match,
45
                'use' => Token::Php_Use,
46
        ];
47

48
        /** @var Token[] */
49
        private array $tokens;
50
        private string $input;
51
        private int $offset;
52
        private Position $position;
53

54

55
        /** @return Token[] */
56
        public function tokenize(string $input, ?Position $position = null): array
1✔
57
        {
58
                $position ??= new Position;
1✔
59
                $this->tokens = $this->tokenizePartially($input, $position, 0);
1✔
60
                if ($this->offset !== strlen($input)) {
1✔
61
                        $token = str_replace("\n", '\n', substr($input, $this->offset, 10));
×
62
                        throw new CompileException("Unexpected '$token'", $position);
×
63
                }
64

65
                $this->tokens[] = new Token(Token::End, '', $position);
1✔
66
                return $this->tokens;
1✔
67
        }
68

69

70
        /** @return Token[] */
71
        public function tokenizePartially(string $input, Position &$position, ?int $ofs = null): array
1✔
72
        {
73
                $this->input = $input;
1✔
74
                $this->offset = $ofs ?? $position->offset;
1✔
75
                $this->position = &$position;
1✔
76
                $this->tokens = [];
1✔
77
                $this->tokenizeCode();
1✔
78
                return $this->tokens;
1✔
79
        }
80

81

82
        /** @return Token[]|null */
83
        public function tokenizeUnquotedString(string $input, Position $position, bool $colon, int $offsetDelta): ?array
1✔
84
        {
85
                preg_match(
1✔
86
                        $colon
1✔
87
                                ? '~ ( [./@_a-z0-9#!-] | :(?!:) | \{\$ [_a-z0-9\[\]()>-]+ })++  (?=\s+[!"\'$(\[{,\\\|\~\w-] | [,|]  | \s*$) ~xAi'
1✔
88
                                : '~ ( [./@_a-z0-9#!-]          | \{\$ [_a-z0-9\[\]()>-]+ })++  (?=\s+[!"\'$(\[{,\\\|\~\w-] | [,:|] | \s*$) ~xAi',
1✔
89
                        $input,
1✔
90
                        $match,
1✔
91
                        offset: $position->offset - $offsetDelta,
1✔
92
                );
93
                $position = new Position($position->line, $position->column - 1, $position->offset - 1);
1✔
94
                return $match && !is_numeric($match[0])
1✔
95
                        ? $this->tokenize('"' . $match[0] . '"', $position)
1✔
96
                        : null;
1✔
97
        }
98

99

100
        private function tokenizeCode(): void
101
        {
102
                $re = <<<'XX'
1✔
103
                        ~(?J)(?n)   # allow duplicate named groups, no auto capture
104
                        (?(DEFINE) (?<label>  [a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*  ) )
105

106
                        (?<Php_Whitespace>  [ \t\r\n]+  )|
107
                        ( (?<Php_ConstantEncapsedString>  '  )  (?<rest>  ( \\. | [^'\\] )*  '  )?  )|
108
                        ( (?<string>  "  )  .*  )|
109
                        ( (?<Php_StartHeredoc>  <<< [ \t]* (?: (?&label) | ' (?&label) ' | " (?&label) " ) \r?\n  ) .*  )|
110
                        ( (?<Php_Comment>  /\*  )   (?<rest>  .*?\*/  )?  )|
111
                        (?<Php_Variable>  \$  (?&label)  )|
112
                        (?<Php_Float>
113
                                ((?&lnum) | (?&dnum)) [eE][+-]? (?&lnum)|
114
                                (?<dnum>   (?&lnum)? \. (?&lnum) | (?&lnum) \. (?&lnum)?  )
115
                        )|
116
                        (?<Php_Integer>
117
                                0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*|
118
                                0[bB][01]+(_[01]+)*|
119
                                0[oO][0-7]+(_[0-7]+)*|
120
                                (?<lnum>  [0-9]+(_[0-9]+)*  )
121
                        )|
122
                        (?<Php_NameFullyQualified>  \\ (?&label) ( \\ (?&label) )*  )|
123
                        (?<Php_NameQualified>  (?&label) ( \\ (?&label) )+  )|
124
                        (?<Php_IdentifierFollowed>  (?&label)  (?= [ \t\r\n]* [(&=] )  )|
125
                        (?<Php_Identifier>  (?&label)((--?|\.)[a-zA-Z0-9_\x80-\xff]+)*  )|
126
                        (
127
                                (
128
                                        (?<Php_ObjectOperator>  ->  )|
129
                                        (?<Php_NullsafeObjectOperator>  \?->  )|
130
                                        (?<Php_UndefinedsafeObjectOperator>  \?\?->  )
131
                                )
132
                                (?<Php_Whitespace>  [ \t\r\n]+  )?
133
                                (?<Php_Identifier>  (?&label)  )?
134
                        )|
135
                        (?<Php_DoubleArrow>  =>  )|
136
                        (?<Php_PlusEqual>  \+=  )|
137
                        (?<Php_MinusEqual>  -=  )|
138
                        (?<Php_MulEqual>  \*=  )|
139
                        (?<Php_DivEqual>  /=  )|
140
                        (?<Php_ConcatEqual>  \.=  )|
141
                        (?<Php_ModEqual>  %=  )|
142
                        (?<Php_AndEqual>  &=  )|
143
                        (?<Php_OrEqual>  \|=  )|
144
                        (?<Php_XorEqual>  \^=  )|
145
                        (?<Php_SlEqual>  <<=  )|
146
                        (?<Php_SrEqual>  >>=  )|
147
                        (?<Php_PowEqual>  \*\*=  )|
148
                        (?<Php_CoalesceEqual>  \?\?=  )|
149
                        (?<Php_Coalesce>  \?\?  )|
150
                        (?<Php_BooleanOr>  \|\|  )|
151
                        (?<Php_BooleanAnd>  &&  )|
152
                        (?<Php_BitwiseOr>  \| (?= [ \t]* (?&Php_Integer) | [ \t]+ [A-Z\\] )  )|
153
                        (?<Php_AmpersandFollowed>  & (?= [ \t\r\n]* (\$|\.\.\.) )  )|
154
                        (?<Php_AmpersandNotFollowed>  &  )|
155
                        (?<Php_IsIdentical>  ===  )|
156
                        (?<Php_IsNotIdentical>  !==  )|
157
                        (?<Php_IsEqual>  ==  )|
158
                        (?<Php_IsNotEqual>  !=  |  <>  )|
159
                        (?<Php_Spaceship>  <=>  )|
160
                        (?<Php_IsSmallerOrEqual>  <=  )|
161
                        (?<Php_IsGreaterOrEqual>  >=  )|
162
                        (?<Php_Sl>  <<  )|
163
                        (?<Php_Sr>  >>  )|
164
                        (?<Php_Inc>  \+\+  )|
165
                        (?<Php_Dec>  --  )|
166
                        (?<Php_Pow>  \*\*  )|
167
                        (?<Php_PaamayimNekudotayim>  ::  )|
168
                        (?<Php_NsSeparator>  \\  )|
169
                        (?<Php_Ellipsis>  \.\.\.  )|
170
                        (?<Php_IntCast>  \( [ \t]* int [ \t]* \)  )|
171
                        (?<Php_FloatCast>  \( [ \t]* float [ \t]* \)  )|
172
                        (?<Php_StringCast>  \( [ \t]* string [ \t]* \)  )|
173
                        (?<Php_ArrayCast>  \( [ \t]* array [ \t]* \)  )|
174
                        (?<Php_ObjectCast>  \( [ \t]* object [ \t]* \)  )|
175
                        (?<Php_BoolCast>  \( [ \t]* bool [ \t]* \)  )|
176
                        (?<Php_ExpandCast>  \( [ \t]* expand [ \t]* \)  )|
177
                        ( (?<end>  /?}  ) .* )|
178
                        (?<char>  [;:,.|^&+/*=%!\~$<>?@#(){[\]-]  )|
179
                        (?<badchar>  .  )
180
                        ~xsA
181
                        XX;
182

183
                $depth = 0;
1✔
184
                matchRE:
185
                preg_match_all($re, $this->input, $matches, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL, $this->offset);
1✔
186
                if (preg_last_error()) {
1✔
UNCOV
187
                        throw new CompileException(preg_last_error_msg());
×
188
                }
189

190
                foreach ($matches as $m) {
1✔
191
                        if (isset($m['char'])) {
1✔
192
                                if ($m['char'] === '{') {
1✔
193
                                        $depth++;
1✔
194
                                }
195
                                $this->addToken(null, $m['char']);
1✔
196

197
                        } elseif (isset($m['end'])) {
1✔
198
                                $depth--;
1✔
199
                                if ($depth < 0) {
1✔
200
                                        return;
1✔
201
                                }
202
                                foreach (str_split($m['end']) as $ch) {
1✔
203
                                        $this->addToken(null, $ch);
1✔
204
                                }
205

206
                                goto matchRE;
1✔
207

208
                        } elseif (isset($m[$type = 'Php_ObjectOperator'])
1✔
209
                                || isset($m[$type = 'Php_NullsafeObjectOperator'])
1✔
210
                                || isset($m[$type = 'Php_UndefinedsafeObjectOperator'])
1✔
211
                        ) {
212
                                $this->addToken(constant(Token::class . '::' . $type), $m[$type]);
1✔
213
                                if (isset($m['Php_Whitespace'])) {
1✔
214
                                        $this->addToken(Token::Php_Whitespace, $m['Php_Whitespace']);
1✔
215
                                }
216
                                if (isset($m['Php_Identifier'])) {
1✔
217
                                        $this->addToken(Token::Php_Identifier, $m['Php_Identifier']);
1✔
218
                                }
219

220
                        } elseif (isset($m['Php_Identifier'])) {
1✔
221
                                $lower = strtolower($m['Php_Identifier']);
1✔
222
                                $this->addToken(
1✔
223
                                        self::Keywords[$lower] ?? (preg_match('~[A-Z_][A-Z0-9_]{2,}$~DA', $m['Php_Identifier']) ? Token::Php_Constant : Token::Php_Identifier),
1✔
224
                                        $m['Php_Identifier'],
1✔
225
                                );
226

227
                        } elseif (isset($m['Php_IdentifierFollowed'])) {
1✔
228
                                $lower = strtolower($m['Php_IdentifierFollowed']);
1✔
229
                                $this->addToken(self::KeywordsFollowed[$lower] ?? self::Keywords[$lower] ?? Token::Php_Identifier, $m['Php_IdentifierFollowed']);
1✔
230

231
                        } elseif (isset($m['Php_ConstantEncapsedString'])) {
1✔
232
                                isset($m['rest'])
1✔
233
                                        ? $this->addToken(Token::Php_ConstantEncapsedString, "'" . $m['rest'])
1✔
234
                                        : throw new CompileException('Unterminated string.', $this->position);
1✔
235

236
                        } elseif (isset($m['string'])) {
1✔
237
                                $pos = $this->position;
1✔
238
                                $this->addToken(null, '"');
1✔
239
                                $count = count($this->tokens);
1✔
240
                                $this->tokenizeString('"');
1✔
241
                                $token = $this->tokens[$count] ?? null;
1✔
242
                                $this->addToken(null, '"');
1✔
243
                                if (
244
                                        count($this->tokens) <= $count + 2
1✔
245
                                        && (!$token || $token->type === Token::Php_EncapsedAndWhitespace)
1✔
246
                                ) {
247
                                        array_splice($this->tokens, $count - 1, null, [new Token(Token::Php_ConstantEncapsedString, '"' . $token?->text . '"', $pos)]);
1✔
248
                                }
249
                                goto matchRE;
1✔
250

251
                        } elseif (isset($m['Php_Integer'])) {
1✔
252
                                $num = PhpHelpers::decodeNumber($m['Php_Integer']);
1✔
253
                                $this->addToken(is_float($num) ? Token::Php_Float : Token::Php_Integer, $m['Php_Integer']);
1✔
254

255
                        } elseif (isset($m['Php_StartHeredoc'])) {
1✔
256
                                $this->addToken(Token::Php_StartHeredoc, $m['Php_StartHeredoc']);
1✔
257
                                $endRe = '(?<=\n)[ \t]*' . trim($m['Php_StartHeredoc'], "< \t\r\n'\"") . '\b';
1✔
258
                                if (str_contains($m['Php_StartHeredoc'], "'")) { // nowdoc
1✔
259
                                        if (!preg_match('~(.*?)(' . $endRe . ')~sA', $this->input, $m, 0, $this->offset)) {
1✔
260
                                                throw new CompileException('Unterminated NOWDOC.', $this->position);
1✔
261
                                        } elseif ($m[1] !== '') {
1✔
262
                                                $this->addToken(Token::Php_EncapsedAndWhitespace, $m[1]);
1✔
263
                                        }
264
                                        $this->addToken(Token::Php_EndHeredoc, $m[2]);
1✔
265
                                } else {
266
                                        $end = $this->tokenizeString($endRe);
1✔
267
                                        $this->addToken(Token::Php_EndHeredoc, $end);
1✔
268
                                }
269
                                goto matchRE;
1✔
270

271
                        } elseif (isset($m['Php_Comment'])) {
1✔
272
                                isset($m['rest'])
1✔
273
                                        ? $this->addToken(Token::Php_Comment, '/*' . $m['rest'])
1✔
274
                                        : throw new CompileException('Unterminated comment.', $this->position);
1✔
275

276
                        } elseif (isset($m['badchar'])) {
1✔
277
                                throw new CompileException("Unexpected '$m[badchar]'", $this->position);
1✔
278

279
                        } else {
280
                                foreach ($m as $type => $text) {
1✔
281
                                        if ($text !== null && !is_int($type)) {
1✔
282
                                                $this->addToken(constant(Token::class . '::' . $type), $text);
1✔
283
                                                break;
1✔
284
                                        }
285
                                }
286
                        }
287
                }
288
        }
1✔
289

290

291
        private function tokenizeString(string $endRe): string
1✔
292
        {
293
                $re = <<<'XX'
1✔
294
                        ~(?J)(?n)   # allow duplicate named groups, no auto capture
295
                        (?(DEFINE) (?<label>  [a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*  ) )
296

297
                        ( (?<Php_CurlyOpen>  \{\$  )  .*  )|
298
                        (?<Php_DollarOpenCurlyBraces>  \$\{  )|
299
                        ( (?<Php_Variable>  \$  (?&label)  )
300
                                (
301
                                        (
302
                                                (?<Php_ObjectOperator>  ->  )|
303
                                                (?<Php_NullsafeObjectOperator>  \?->  )|
304
                                                (?<Php_UndefinedsafeObjectOperator>  \?\?->  )
305
                                        )
306
                                        (?<Php_Identifier>  (?&label)  )
307
                                        |
308
                                        (?<offset>  \[  )
309
                                        (
310
                                                (?<offsetVar>  \$  (?&label)  )|
311
                                                (?<offsetString>  (?&label)  )|
312
                                                (?<offsetMinus>  -  )?
313
                                                (?<Php_NumString>
314
                                                        0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*|
315
                                                        0[bB][01]+(_[01]+)*|
316
                                                        0[oO][0-7]+(_[0-7]+)*|
317
                                                        [0-9]+(_[0-9]+)*
318
                                                )
319
                                        )?
320
                                        (?<offsetEnd>  ]  )?
321
                                        |
322
                                )
323
                        )|
324
                        XX . "
1✔
325
                        ((?<end>  $endRe  )  .*  )|
1✔
326
                        (?<char>  ( \\\\. | [^\\\\] )  )
327
                        ~xsA";
328

329
                matchRE:
330
                preg_match_all($re, $this->input, $matches, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL, $this->offset);
1✔
331
                if (preg_last_error()) {
1✔
UNCOV
332
                        throw new CompileException(preg_last_error_msg());
×
333
                }
334

335
                $buffer = '';
1✔
336
                foreach ($matches as $m) {
1✔
337
                        if (isset($m['char'])) {
1✔
338
                                $buffer .= $m['char'];
1✔
339
                                continue;
1✔
340
                        } elseif ($buffer !== '') {
1✔
341
                                $this->addToken(Token::Php_EncapsedAndWhitespace, $buffer);
1✔
342
                                $buffer = '';
1✔
343
                        }
344

345
                        if (isset($m['Php_CurlyOpen'])) {
1✔
346
                                $this->addToken(Token::Php_CurlyOpen, '{');
1✔
347
                                $this->tokenizeCode();
1✔
348
                                if (($this->input[$this->offset] ?? null) === '}') {
1✔
349
                                        $this->addToken(null, '}');
1✔
350
                                }
351
                                goto matchRE;
1✔
352

353
                        } elseif (isset($m['Php_DollarOpenCurlyBraces'])) {
1✔
354
                                throw new CompileException('Syntax ${...} is not supported.', $this->position);
1✔
355

356
                        } elseif (isset($m['Php_Variable'])) {
1✔
357
                                $this->addToken(Token::Php_Variable, $m['Php_Variable']);
1✔
358
                                if (isset($m[$type = 'Php_ObjectOperator'])
1✔
359
                                        || isset($m[$type = 'Php_NullsafeObjectOperator'])
1✔
360
                                        || isset($m[$type = 'Php_UndefinedsafeObjectOperator'])
1✔
361
                                ) {
362
                                        $this->addToken(constant(Token::class . '::' . $type), $m[$type]);
1✔
363
                                        $this->addToken(Token::Php_Identifier, $m['Php_Identifier']);
1✔
364

365
                                } elseif (isset($m['offset'])) {
1✔
366
                                        $this->addToken(null, '[');
1✔
367
                                        if (!isset($m['offsetEnd'])) {
1✔
368
                                                throw new CompileException("Missing ']'", $this->position);
1✔
369
                                        } elseif (isset($m['offsetVar'])) {
1✔
370
                                                $this->addToken(Token::Php_Variable, $m['offsetVar']);
1✔
371
                                        } elseif (isset($m['offsetString'])) {
1✔
372
                                                $this->addToken(Token::Php_Identifier, $m['offsetString']);
1✔
373
                                        } elseif (isset($m['Php_NumString'])) {
1✔
374
                                                if (isset($m['offsetMinus'])) {
1✔
375
                                                        $this->addToken(null, '-');
1✔
376
                                                }
377
                                                $this->addToken(Token::Php_NumString, $m['Php_NumString']);
1✔
378
                                        } else {
379
                                                throw new CompileException("Unexpected '" . substr($this->input, $this->offset - 1, 5) . "'", $this->position);
1✔
380
                                        }
381
                                        $this->addToken(null, ']');
1✔
382
                                }
383

384
                        } elseif (isset($m['end'])) {
1✔
385
                                return $m['end'];
1✔
386
                        }
387
                }
388

389
                throw new CompileException('Unterminated string.', $this->position->advance($buffer));
1✔
390
        }
391

392

393
        private function addToken(?int $type, string $text): void
1✔
394
        {
395
                $this->tokens[] = new Token($type ?? ord($text), $text, $this->position);
1✔
396
                $this->position = $this->position->advance($text);
1✔
397
                $this->offset += strlen($text);
1✔
398
        }
1✔
399
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc