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

MyIntervals / PHP-CSS-Parser / 13442343631

20 Feb 2025 06:39PM UTC coverage: 53.778% (+0.03%) from 53.749%
13442343631

push

github

web-flow
[CLEANUP] Don't store array length as a property (#954)

Since PHP 5, if not earlier, the length of an array is internally stored by PHP
and can be accessed in O(1) time.
Maintaining both the array and its length in class properties is error-prone
(particularly regarding possible future code changes).
When used in a loop, `\count($array)` is more repetitively expensive than
`$arrayLength`,
but the latter can be set up as a local variable as and when needed.

Reference:
https://stackoverflow.com/questions/5835241/is-phps-count-function-o1-or-on-for-arrays

Resolves #953.

0 of 7 new or added lines in 1 file covered. (0.0%)

1025 of 1906 relevant lines covered (53.78%)

11.98 hits per line

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

0.0
/src/Parsing/ParserState.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Sabberworm\CSS\Parsing;
6

7
use Sabberworm\CSS\Comment\Comment;
8
use Sabberworm\CSS\Settings;
9

10
/**
11
 * @internal since 8.7.0
12
 */
13
class ParserState
14
{
15
    /**
16
     * @var null
17
     */
18
    public const EOF = null;
19

20
    /**
21
     * @var Settings
22
     */
23
    private $parserSettings;
24

25
    /**
26
     * @var string
27
     */
28
    private $text;
29

30
    /**
31
     * @var array<int, string>
32
     */
33
    private $characters;
34

35
    /**
36
     * @var int
37
     */
38
    private $currentPosition = 0;
39

40
    /**
41
     * will only be used if the CSS does not contain an `@charset` declaration
42
     *
43
     * @var string
44
     */
45
    private $charset;
46

47
    /**
48
     * @var int
49
     */
50
    private $lineNumber;
51

52
    /**
53
     * @param string $text the complete CSS as text (i.e., usually the contents of a CSS file)
54
     * @param int<0, max> $lineNumber
55
     */
56
    public function __construct($text, Settings $parserSettings, $lineNumber = 1)
×
57
    {
58
        $this->parserSettings = $parserSettings;
×
59
        $this->text = $text;
×
60
        $this->lineNumber = $lineNumber;
×
61
        $this->setCharset($this->parserSettings->sDefaultCharset);
×
62
    }
×
63

64
    /**
65
     * Sets the charset to be used if the CSS does not contain an `@charset` declaration.
66
     *
67
     * @throws SourceException if the charset is UTF-8 and the content has invalid byte sequences
68
     */
69
    public function setCharset(string $charset): void
×
70
    {
71
        $this->charset = $charset;
×
72
        $this->characters = $this->strsplit($this->text);
×
73
    }
×
74

75
    /**
76
     * @return int
77
     */
78
    public function currentLine()
×
79
    {
80
        return $this->lineNumber;
×
81
    }
82

83
    /**
84
     * @return int
85
     */
86
    public function currentColumn()
×
87
    {
88
        return $this->currentPosition;
×
89
    }
90

91
    /**
92
     * @return Settings
93
     */
94
    public function getSettings()
×
95
    {
96
        return $this->parserSettings;
×
97
    }
98

99
    public function anchor(): Anchor
×
100
    {
101
        return new Anchor($this->currentPosition, $this);
×
102
    }
103

104
    /**
105
     * @param int $position
106
     */
107
    public function setPosition($position): void
×
108
    {
109
        $this->currentPosition = $position;
×
110
    }
×
111

112
    /**
113
     * @param bool $ignoreCase
114
     *
115
     * @return string
116
     *
117
     * @throws UnexpectedTokenException
118
     */
119
    public function parseIdentifier($ignoreCase = true)
×
120
    {
121
        if ($this->isEnd()) {
×
122
            throw new UnexpectedEOFException('', '', 'identifier', $this->lineNumber);
×
123
        }
124
        $result = $this->parseCharacter(true);
×
125
        if ($result === null) {
×
126
            throw new UnexpectedTokenException('', $this->peek(5), 'identifier', $this->lineNumber);
×
127
        }
128
        $character = null;
×
129
        while (!$this->isEnd() && ($character = $this->parseCharacter(true)) !== null) {
×
130
            if (\preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $character)) {
×
131
                $result .= $character;
×
132
            } else {
133
                $result .= '\\' . $character;
×
134
            }
135
        }
136
        if ($ignoreCase) {
×
137
            $result = $this->strtolower($result);
×
138
        }
139
        return $result;
×
140
    }
141

142
    /**
143
     * @param bool $isForIdentifier
144
     *
145
     * @return string|null
146
     *
147
     * @throws UnexpectedEOFException
148
     * @throws UnexpectedTokenException
149
     */
150
    public function parseCharacter($isForIdentifier)
×
151
    {
152
        if ($this->peek() === '\\') {
×
153
            if (
154
                $isForIdentifier && $this->parserSettings->bLenientParsing
×
155
                && ($this->comes('\\0') || $this->comes('\\9'))
×
156
            ) {
157
                // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
158
                return null;
×
159
            }
160
            $this->consume('\\');
×
161
            if ($this->comes('\\n') || $this->comes('\\r')) {
×
162
                return '';
×
163
            }
164
            if (\preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
×
165
                return $this->consume(1);
×
166
            }
167
            $hexCodePoint = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
×
168
            if ($this->strlen($hexCodePoint) < 6) {
×
169
                // Consume whitespace after incomplete unicode escape
170
                if (\preg_match('/\\s/isSu', $this->peek())) {
×
171
                    if ($this->comes('\\r\\n')) {
×
172
                        $this->consume(2);
×
173
                    } else {
174
                        $this->consume(1);
×
175
                    }
176
                }
177
            }
178
            $codePoint = \intval($hexCodePoint, 16);
×
179
            $utf32EncodedCharacter = '';
×
180
            for ($i = 0; $i < 4; ++$i) {
×
181
                $utf32EncodedCharacter .= \chr($codePoint & 0xff);
×
182
                $codePoint = $codePoint >> 8;
×
183
            }
184
            return \iconv('utf-32le', $this->charset, $utf32EncodedCharacter);
×
185
        }
186
        if ($isForIdentifier) {
×
187
            $peek = \ord($this->peek());
×
188
            // Ranges: a-z A-Z 0-9 - _
189
            if (
190
                ($peek >= 97 && $peek <= 122)
×
191
                || ($peek >= 65 && $peek <= 90)
×
192
                || ($peek >= 48 && $peek <= 57)
×
193
                || ($peek === 45)
×
194
                || ($peek === 95)
×
195
                || ($peek > 0xa1)
×
196
            ) {
197
                return $this->consume(1);
×
198
            }
199
        } else {
200
            return $this->consume(1);
×
201
        }
202
        return null;
×
203
    }
204

205
    /**
206
     * @return array<int, Comment>|void
207
     *
208
     * @throws UnexpectedEOFException
209
     * @throws UnexpectedTokenException
210
     */
211
    public function consumeWhiteSpace(): array
×
212
    {
213
        $comments = [];
×
214
        do {
215
            while (\preg_match('/\\s/isSu', $this->peek()) === 1) {
×
216
                $this->consume(1);
×
217
            }
218
            if ($this->parserSettings->bLenientParsing) {
×
219
                try {
220
                    $comment = $this->consumeComment();
×
221
                } catch (UnexpectedEOFException $e) {
×
NEW
222
                    $this->currentPosition = \count($this->characters);
×
223
                    return $comments;
×
224
                }
225
            } else {
226
                $comment = $this->consumeComment();
×
227
            }
228
            if ($comment !== false) {
×
229
                $comments[] = $comment;
×
230
            }
231
        } while ($comment !== false);
×
232
        return $comments;
×
233
    }
234

235
    /**
236
     * @param string $sString
237
     * @param bool $bCaseInsensitive
238
     */
239
    public function comes($sString, $bCaseInsensitive = false): bool
×
240
    {
241
        $sPeek = $this->peek(\strlen($sString));
×
242
        return ($sPeek == '')
×
243
            ? false
×
244
            : $this->streql($sPeek, $sString, $bCaseInsensitive);
×
245
    }
246

247
    /**
248
     * @param int $length
249
     * @param int $offset
250
     */
251
    public function peek($length = 1, $offset = 0): string
×
252
    {
253
        $offset += $this->currentPosition;
×
NEW
254
        if ($offset >= \count($this->characters)) {
×
255
            return '';
×
256
        }
257
        return $this->substr($offset, $length);
×
258
    }
259

260
    /**
261
     * @param int $mValue
262
     *
263
     * @throws UnexpectedEOFException
264
     * @throws UnexpectedTokenException
265
     */
266
    public function consume($mValue = 1): string
×
267
    {
268
        if (\is_string($mValue)) {
×
269
            $iLineCount = \substr_count($mValue, "\n");
×
270
            $length = $this->strlen($mValue);
×
271
            if (!$this->streql($this->substr($this->currentPosition, $length), $mValue)) {
×
272
                throw new UnexpectedTokenException(
×
273
                    $mValue,
×
274
                    $this->peek(\max($length, 5)),
×
275
                    'literal',
×
276
                    $this->lineNumber
×
277
                );
278
            }
279
            $this->lineNumber += $iLineCount;
×
280
            $this->currentPosition += $this->strlen($mValue);
×
281
            return $mValue;
×
282
        } else {
NEW
283
            if ($this->currentPosition + $mValue > \count($this->characters)) {
×
284
                throw new UnexpectedEOFException((string) $mValue, $this->peek(5), 'count', $this->lineNumber);
×
285
            }
286
            $result = $this->substr($this->currentPosition, $mValue);
×
287
            $iLineCount = \substr_count($result, "\n");
×
288
            $this->lineNumber += $iLineCount;
×
289
            $this->currentPosition += $mValue;
×
290
            return $result;
×
291
        }
292
    }
293

294
    /**
295
     * @param string $mExpression
296
     * @param int|null $iMaxLength
297
     *
298
     * @throws UnexpectedEOFException
299
     * @throws UnexpectedTokenException
300
     */
301
    public function consumeExpression($mExpression, $iMaxLength = null): string
×
302
    {
303
        $aMatches = null;
×
304
        $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
×
305
        if (\preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
×
306
            return $this->consume($aMatches[0][0]);
×
307
        }
308
        throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->lineNumber);
×
309
    }
310

311
    /**
312
     * @return Comment|false
313
     */
314
    public function consumeComment()
×
315
    {
316
        $mComment = false;
×
317
        if ($this->comes('/*')) {
×
318
            $lineNumber = $this->lineNumber;
×
319
            $this->consume(1);
×
320
            $mComment = '';
×
321
            while (($char = $this->consume(1)) !== '') {
×
322
                $mComment .= $char;
×
323
                if ($this->comes('*/')) {
×
324
                    $this->consume(2);
×
325
                    break;
×
326
                }
327
            }
328
        }
329

330
        if ($mComment !== false) {
×
331
            // We skip the * which was included in the comment.
332
            return new Comment(\substr($mComment, 1), $lineNumber);
×
333
        }
334

335
        return $mComment;
×
336
    }
337

338
    public function isEnd(): bool
×
339
    {
NEW
340
        return $this->currentPosition >= \count($this->characters);
×
341
    }
342

343
    /**
344
     * @param array<array-key, string>|string $aEnd
345
     * @param string $bIncludeEnd
346
     * @param string $consumeEnd
347
     * @param array<int, Comment> $comments
348
     *
349
     * @throws UnexpectedEOFException
350
     * @throws UnexpectedTokenException
351
     */
352
    public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = []): string
×
353
    {
354
        $aEnd = \is_array($aEnd) ? $aEnd : [$aEnd];
×
355
        $out = '';
×
356
        $start = $this->currentPosition;
×
357

358
        while (!$this->isEnd()) {
×
359
            $char = $this->consume(1);
×
360
            if (\in_array($char, $aEnd, true)) {
×
361
                if ($bIncludeEnd) {
×
362
                    $out .= $char;
×
363
                } elseif (!$consumeEnd) {
×
364
                    $this->currentPosition -= $this->strlen($char);
×
365
                }
366
                return $out;
×
367
            }
368
            $out .= $char;
×
369
            if ($comment = $this->consumeComment()) {
×
370
                $comments[] = $comment;
×
371
            }
372
        }
373

374
        if (\in_array(self::EOF, $aEnd, true)) {
×
375
            return $out;
×
376
        }
377

378
        $this->currentPosition = $start;
×
379
        throw new UnexpectedEOFException(
×
380
            'One of ("' . \implode('","', $aEnd) . '")',
×
381
            $this->peek(5),
×
382
            'search',
×
383
            $this->lineNumber
×
384
        );
385
    }
386

387
    private function inputLeft(): string
×
388
    {
389
        return $this->substr($this->currentPosition, -1);
×
390
    }
391

392
    /**
393
     * @param string $sString1
394
     * @param string $sString2
395
     * @param bool $bCaseInsensitive
396
     */
397
    public function streql($sString1, $sString2, $bCaseInsensitive = true): bool
×
398
    {
399
        if ($bCaseInsensitive) {
×
400
            return $this->strtolower($sString1) === $this->strtolower($sString2);
×
401
        } else {
402
            return $sString1 === $sString2;
×
403
        }
404
    }
405

406
    /**
407
     * @param int $iAmount
408
     */
409
    public function backtrack($iAmount): void
×
410
    {
411
        $this->currentPosition -= $iAmount;
×
412
    }
×
413

414
    /**
415
     * @param string $sString
416
     */
417
    public function strlen($sString): int
×
418
    {
419
        if ($this->parserSettings->bMultibyteSupport) {
×
420
            return \mb_strlen($sString, $this->charset);
×
421
        } else {
422
            return \strlen($sString);
×
423
        }
424
    }
425

426
    /**
427
     * @param int $iStart
428
     * @param int $length
429
     */
430
    private function substr($iStart, $length): string
×
431
    {
432
        if ($length < 0) {
×
NEW
433
            $length = \count($this->characters) - $iStart + $length;
×
434
        }
NEW
435
        if ($iStart + $length > \count($this->characters)) {
×
NEW
436
            $length = \count($this->characters) - $iStart;
×
437
        }
438
        $result = '';
×
439
        while ($length > 0) {
×
440
            $result .= $this->characters[$iStart];
×
441
            $iStart++;
×
442
            $length--;
×
443
        }
444
        return $result;
×
445
    }
446

447
    /**
448
     * @param string $sString
449
     */
450
    private function strtolower($sString): string
×
451
    {
452
        if ($this->parserSettings->bMultibyteSupport) {
×
453
            return \mb_strtolower($sString, $this->charset);
×
454
        } else {
455
            return \strtolower($sString);
×
456
        }
457
    }
458

459
    /**
460
     * @param string $sString
461
     *
462
     * @return array<int, string>
463
     *
464
     * @throws SourceException if the charset is UTF-8 and the string contains invalid byte sequences
465
     */
466
    private function strsplit($sString)
×
467
    {
468
        if ($this->parserSettings->bMultibyteSupport) {
×
469
            if ($this->streql($this->charset, 'utf-8')) {
×
470
                $result = \preg_split('//u', $sString, -1, PREG_SPLIT_NO_EMPTY);
×
471
                if (!\is_array($result)) {
×
472
                    throw new SourceException('`preg_split` failed with error ' . \preg_last_error());
×
473
                }
474
                return $result;
×
475
            } else {
476
                $length = \mb_strlen($sString, $this->charset);
×
477
                $result = [];
×
478
                for ($i = 0; $i < $length; ++$i) {
×
479
                    $result[] = \mb_substr($sString, $i, 1, $this->charset);
×
480
                }
481
                return $result;
×
482
            }
483
        } else {
484
            if ($sString === '') {
×
485
                return [];
×
486
            } else {
487
                return \str_split($sString);
×
488
            }
489
        }
490
    }
491

492
    /**
493
     * @param string $sString
494
     * @param string $sNeedle
495
     * @param int $offset
496
     *
497
     * @return int|false
498
     */
499
    private function strpos($sString, $sNeedle, $offset)
×
500
    {
501
        if ($this->parserSettings->bMultibyteSupport) {
×
502
            return \mb_strpos($sString, $sNeedle, $offset, $this->charset);
×
503
        } else {
504
            return \strpos($sString, $sNeedle, $offset);
×
505
        }
506
    }
507
}
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