Completed
Push — master ( fd2bc2...1dc9e9 )
by Marcus
03:01
created

Parser::color()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 1
crap 3
1
<?php
2
3
namespace LesserPhp;
4
5
use LesserPhp\Exception\GeneralException;
6
7
/**
8
 * lesserphp
9
 * https://www.maswaba.de/lesserphp
10
 *
11
 * LESS CSS compiler, adapted from http://lesscss.org
12
 *
13
 * Copyright 2013, Leaf Corcoran <[email protected]>
14
 * Copyright 2016, Marcus Schwarz <[email protected]>
15
 * Licensed under MIT or GPLv3, see LICENSE
16
 * @package LesserPhp
17
 * // responsible for taking a string of LESS code and converting it into a
18
 * // syntax tree
19
 */
20
class Parser
21
{
22
23
    static protected $nextBlockId = 0; // used to uniquely identify blocks
24
25
    static protected $precedence = [
26
        '=<' => 0,
27
        '>=' => 0,
28
        '=' => 0,
29
        '<' => 0,
30
        '>' => 0,
31
32
        '+' => 1,
33
        '-' => 1,
34
        '*' => 2,
35
        '/' => 2,
36
        '%' => 2,
37
    ];
38
39
    static protected $whitePattern;
40
    static protected $commentMulti;
41
42
    static protected $commentSingle = "//";
43
    static protected $commentMultiLeft = "/*";
44
    static protected $commentMultiRight = "*/";
45
46
    // regex string to match any of the operators
47
    static protected $operatorString;
48
49
    // these properties will supress division unless it's inside parenthases
50
    static protected $supressDivisionProps =
51
        ['/border-radius$/i', '/^font$/i'];
52
53
    private $blockDirectives = [
54
        'font-face',
55
        'keyframes',
56
        'page',
57
        '-moz-document',
58
        'viewport',
59
        '-moz-viewport',
60
        '-o-viewport',
61
        '-ms-viewport',
62
    ];
63
    private $lineDirectives = ['charset'];
64
65
    /**
66
     * if we are in parens we can be more liberal with whitespace around
67
     * operators because it must evaluate to a single value and thus is less
68
     * ambiguous.
69
     *
70
     * Consider:
71
     *     property1: 10 -5; // is two numbers, 10 and -5
72
     *     property2: (10 -5); // should evaluate to 5
73
     */
74
    protected $inParens = false;
75
76
    // caches preg escaped literals
77
    static protected $literalCache = [];
78
    /** @var int */
79
    public $count;
80
    /** @var int */
81
    private $line;
82
    /** @var array */
83
    private $seenComments;
84
    /** @var string */
85
    public $buffer;
86
87
    private $env;
88
    /** @var bool */
89
    private $inExp;
90
    /** @var string */
91
    private $currentProperty;
92
93
    /**
94
     * @var bool
95
     */
96
    private $writeComments = false;
97
98
    /**
99
     * Parser constructor.
100
     *
101
     * @param \LesserPhp\Compiler $lessc
102
     * @param null                $sourceName
103
     */
104 49
    public function __construct(Compiler $lessc, $sourceName = null)
105
    {
106 49
        $this->eatWhiteDefault = true;
0 ignored issues
show
Bug introduced by
The property eatWhiteDefault does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
107
        // reference to less needed for vPrefix, mPrefix, and parentSelector
108 49
        $this->lessc = $lessc;
0 ignored issues
show
Bug introduced by
The property lessc does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
109
110 49
        $this->sourceName = $sourceName; // name used for error messages
0 ignored issues
show
Bug introduced by
The property sourceName does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
111
112 49
        if (!self::$operatorString) {
113 1
            self::$operatorString =
114 1
                '(' . implode('|', array_map([Compiler::class, 'pregQuote'], array_keys(self::$precedence))) . ')';
115
116 1
            $commentSingle = Compiler::pregQuote(self::$commentSingle);
117 1
            $commentMultiLeft = Compiler::pregQuote(self::$commentMultiLeft);
118 1
            $commentMultiRight = Compiler::pregQuote(self::$commentMultiRight);
119
120 1
            self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight;
121 1
            self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais';
122
        }
123 49
    }
124
125
    /**
126
     * @param $buffer
127
     *
128
     * @return mixed
129
     * @throws \LesserPhp\Exception\GeneralException
130
     */
131 49
    public function parse($buffer)
132
    {
133 49
        $this->count = 0;
134 49
        $this->line = 1;
135
136 49
        $this->env = null; // block stack
137 49
        $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
138 49
        $this->pushSpecialBlock('root');
139 49
        $this->eatWhiteDefault = true;
140 49
        $this->seenComments = [];
141
142 49
        $this->whitespace();
143
144
        // parse the entire file
145 49
        while (false !== $this->parseChunk()) {
146
            ;
147
        }
148
149 49
        if ($this->count !== strlen($this->buffer)) {
150
//            var_dump($this->count);
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% 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...
151
//            var_dump($this->buffer);
152
            $this->throwError();
153
        }
154
155
        // TODO report where the block was opened
156 49
        if (!property_exists($this->env, 'parent') || $this->env->parent !== null) {
157
            throw new GeneralException('parse error: unclosed block');
158
        }
159
160 49
        return $this->env;
161
    }
162
163
    /**
164
     * Parse a single chunk off the head of the buffer and append it to the
165
     * current parse environment.
166
     * Returns false when the buffer is empty, or when there is an error.
167
     *
168
     * This function is called repeatedly until the entire document is
169
     * parsed.
170
     *
171
     * This parser is most similar to a recursive descent parser. Single
172
     * functions represent discrete grammatical rules for the language, and
173
     * they are able to capture the text that represents those rules.
174
     *
175
     * Consider the function \LesserPhp\Compiler::keyword(). (all parse functions are
176
     * structured the same)
177
     *
178
     * The function takes a single reference argument. When calling the
179
     * function it will attempt to match a keyword on the head of the buffer.
180
     * If it is successful, it will place the keyword in the referenced
181
     * argument, advance the position in the buffer, and return true. If it
182
     * fails then it won't advance the buffer and it will return false.
183
     *
184
     * All of these parse functions are powered by \LesserPhp\Compiler::match(), which behaves
185
     * the same way, but takes a literal regular expression. Sometimes it is
186
     * more convenient to use match instead of creating a new function.
187
     *
188
     * Because of the format of the functions, to parse an entire string of
189
     * grammatical rules, you can chain them together using &&.
190
     *
191
     * But, if some of the rules in the chain succeed before one fails, then
192
     * the buffer position will be left at an invalid state. In order to
193
     * avoid this, \LesserPhp\Compiler::seek() is used to remember and set buffer positions.
194
     *
195
     * Before parsing a chain, use $s = $this->seek() to remember the current
196
     * position into $s. Then if a chain fails, use $this->seek($s) to
197
     * go back where we started.
198
     * @throws \LesserPhp\Exception\GeneralException
199
     */
200 49
    protected function parseChunk()
201
    {
202 49
        if (empty($this->buffer)) {
203
            return false;
204
        }
205 49
        $s = $this->seek();
206
207 49
        if ($this->whitespace()) {
208 46
            return true;
209
        }
210
211
        // setting a property
212 49 View Code Duplication
        if ($this->keyword($key) && $this->assign() && $this->propertyValue($value, $key) && $this->end()) {
0 ignored issues
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...
213 47
            $this->append(['assign', $key, $value], $s);
214
215 47
            return true;
216
        } else {
217 49
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 205 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
218
        }
219
220
        // look for special css blocks
221 49
        if ($this->literal('@', false)) {
222 26
            $this->count--;
223
224
            // media
225 26
            if ($this->literal('@media')) {
226 3
                return $this->handleLiteralMedia($s);
227
            }
228
229 26
            if ($this->literal('@', false) && $this->keyword($directiveName)) {
230 26
                if ($this->isDirective($directiveName, $this->blockDirectives)) {
231 4
                    if ($this->handleDirectiveBlock($directiveName) === true) {
232 4
                        return true;
233
                    }
234 25
                } elseif ($this->isDirective($directiveName, $this->lineDirectives)) {
235 1
                    if ($this->handleDirectiveLine($directiveName) === true) {
236 1
                        return true;
237
                    }
238 25
                } elseif ($this->literal(':', true)) {
239 24
                    if ($this->handleRulesetDefinition($directiveName) === true) {
240 1
                        return true;
241
                    }
242
                }
243
            }
244
245 25
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 205 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
246
        }
247
248 49
        if ($this->literal('&', false)) {
249 4
            $this->count--;
250 4
            if ($this->literal('&:extend')) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
251
                // hierauf folgt was in runden klammern, und zwar das element, das erweitert werden soll
252
                // heißt also, das was in klammern steht wird um die aktuellen klassen erweitert
253
                /*
254
Aus
255
256
nav ul {
257
  &:extend(.inline);
258
  background: blue;
259
}
260
.inline {
261
  color: red;
262
}
263
264
265
Wird:
266
267
nav ul {
268
  background: blue;
269
}
270
.inline,
271
nav ul {
272
  color: red;
273
}
274
275
                 */
276
//                echo "Here we go";
277
            }
278
        }
279
280
281
        // setting a variable
282 49 View Code Duplication
        if ($this->variable($var) && $this->assign() &&
0 ignored issues
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...
283 49
            $this->propertyValue($value) && $this->end()
284
        ) {
285 23
            $this->append(['assign', $var, $value], $s);
286
287 23
            return true;
288
        } else {
289 49
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 205 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
290
        }
291
292 49
        if ($this->import($importValue)) {
293 3
            $this->append($importValue, $s);
294
295 3
            return true;
296
        }
297
298
        // opening parametric mixin
299 49
        if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
300 49
            ($this->guards($guards) || true) &&
301 49
            $this->literal('{')
302
        ) {
303 18
            $block = $this->pushBlock($this->fixTags([$tag]));
304 18
            $block->args = $args;
305 18
            $block->isVararg = $isVararg;
306 18
            if (!empty($guards)) {
307 5
                $block->guards = $guards;
308
            }
309
310 18
            return true;
311
        } else {
312 49
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 205 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
313
        }
314
315
        // opening a simple block
316 49
        if ($this->tags($tags) && $this->literal('{', false)) {
317 46
            $tags = $this->fixTags($tags);
318 46
            $this->pushBlock($tags);
319
320 46
            return true;
321
        } else {
322 49
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 205 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
323
        }
324
325
        // closing a block
326 49
        if ($this->literal('}', false)) {
327
            try {
328 46
                $block = $this->pop();
329
            } catch (\Exception $e) {
330
                $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 205 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
331
                $this->throwError($e->getMessage());
332
            }
333
334 46
            $hidden = false;
335 46
            if ($block->type === null) {
336 46
                $hidden = true;
337 46
                if (!isset($block->args)) {
338 46
                    foreach ($block->tags as $tag) {
339 46
                        if (!is_string($tag) || $tag[0] !== $this->lessc->getMPrefix()) {
340 46
                            $hidden = false;
341 46
                            break;
342
                        }
343
                    }
344
                }
345
346 46
                foreach ($block->tags as $tag) {
347 46
                    if (is_string($tag)) {
348 46
                        $this->env->children[$tag][] = $block;
349
                    }
350
                }
351
            }
352
353 46
            if (!$hidden) {
354 46
                $this->append(['block', $block], $s);
355
            }
356
357
            // this is done here so comments aren't bundled into he block that
358
            // was just closed
359 46
            $this->whitespace();
360
361 46
            return true;
362
        }
363
364
        // mixin
365 49
        if ($this->mixinTags($tags) &&
366 49
            ($this->argumentDef($argv, $isVararg) || true) &&
367 49
            ($this->keyword($suffix) || true) && $this->end()
368
        ) {
369 22
            $tags = $this->fixTags($tags);
370 22
            $this->append(['mixin', $tags, $argv, $suffix], $s);
371
372 22
            return true;
373
        } else {
374 49
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 205 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
375
        }
376
377
        // spare ;
378 49
        if ($this->literal(';')) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return $this->literal(';');.
Loading history...
379 4
            return true;
380
        }
381
382 49
        return false; // got nothing, throw error
383
    }
384
385
    /**
386
     * @param string $directiveName
387
     * @param array $directives
388
     *
389
     * @return bool
390
     */
391 26
    protected function isDirective($directiveName, array $directives)
392
    {
393
        // TODO: cache pattern in parser
394 26
        $pattern = implode('|', array_map([Compiler::class, 'pregQuote'], $directives));
395 26
        $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
396
397 26
        return (preg_match($pattern, $directiveName) === 1);
398
    }
399
400
    /**
401
     * @param array $tags
402
     *
403
     * @return mixed
404
     */
405 46
    protected function fixTags(array $tags)
406
    {
407
        // move @ tags out of variable namespace
408 46
        foreach ($tags as &$tag) {
409 46
            if ($tag[0] === $this->lessc->getVPrefix()) {
410 46
                $tag[0] = $this->lessc->getMPrefix();
411
            }
412
        }
413
414 46
        return $tags;
415
    }
416
417
    /**
418
     * a list of expressions
419
     *
420
     * @param $exps
421
     *
422
     * @return bool
423
     */
424 48
    protected function expressionList(&$exps)
425
    {
426 48
        $values = [];
427
428 48
        while ($this->expression($exp)) {
429 48
            $values[] = $exp;
430
        }
431
432 48
        if (count($values) === 0) {
433 26
            return false;
434
        }
435
436 48
        $exps = Compiler::compressList($values, ' ');
437
438 48
        return true;
439
    }
440
441
    /**
442
     * Attempt to consume an expression.
443
     * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
444
     *
445
     * @param $out
446
     *
447
     * @return bool
448
     */
449 48
    protected function expression(&$out)
450
    {
451 48
        if ($this->value($lhs)) {
452 48
            $out = $this->expHelper($lhs, 0);
453
454
            // look for / shorthand
455 48
            if (!empty($this->env->supressedDivision)) {
456 2
                unset($this->env->supressedDivision);
457 2
                $s = $this->seek();
458 2
                if ($this->literal("/") && $this->value($rhs)) {
459
                    $out = [
460 2
                        "list",
461 2
                        "",
462 2
                        [$out, ["keyword", "/"], $rhs],
463
                    ];
464
                } else {
465
                    $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 457 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
466
                }
467
            }
468
469 48
            return true;
470
        }
471
472 48
        return false;
473
    }
474
475
    /**
476
     * recursively parse infix equation with $lhs at precedence $minP
477
     *
478
     * @param $lhs
479
     * @param $minP
480
     *
481
     * @return array
482
     */
483 48
    protected function expHelper($lhs, $minP)
484
    {
485 48
        $this->inExp = true;
486 48
        $ss = $this->seek();
487
488 48
        while (true) {
489 48
            $whiteBefore = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]);
490
491
            // If there is whitespace before the operator, then we require
492
            // whitespace after the operator for it to be an expression
493 48
            $needWhite = $whiteBefore && !$this->inParens;
494
495 48
            if ($this->match(self::$operatorString . ($needWhite ? '\s' : ''), $m) &&
496 48
                self::$precedence[$m[1]] >= $minP
497
            ) {
498 20
                if (!$this->inParens &&
499 20
                    isset($this->env->currentProperty) &&
500 20
                    $m[1] === '/' &&
501 20
                    empty($this->env->supressedDivision)
502
                ) {
503 5
                    foreach (self::$supressDivisionProps as $pattern) {
504 5
                        if (preg_match($pattern, $this->env->currentProperty)) {
505 2
                            $this->env->supressedDivision = true;
506 5
                            break 2;
507
                        }
508
                    }
509
                }
510
511 20
                $whiteAfter = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]);
512
513 20
                if (!$this->value($rhs)) {
514
                    break;
515
                }
516
517
                // peek for next operator to see what to do with rhs
518 20
                if ($this->peek(self::$operatorString, $next) &&
519 20
                    self::$precedence[$next[1]] > self::$precedence[$m[1]]
520
                ) {
521 1
                    $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
522
                }
523
524 20
                $lhs = ['expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter];
525 20
                $ss = $this->seek();
526
527 20
                continue;
528
            }
529
530 48
            break;
531
        }
532
533 48
        $this->seek($ss);
0 ignored issues
show
Bug introduced by
It seems like $ss can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
534
535 48
        return $lhs;
536
    }
537
538
    /**
539
     * consume a list of values for a property
540
     *
541
     * @param      $value
542
     * @param string $keyName
543
     *
544
     * @return bool
545
     */
546 48
    public function propertyValue(&$value, $keyName = null)
547
    {
548 48
        $values = [];
549
550 48
        if ($keyName !== null) {
551 47
            $this->env->currentProperty = $keyName;
552
        }
553
554 48
        $s = null;
555 48
        while ($this->expressionList($v)) {
556 48
            $values[] = $v;
557 48
            $s = $this->seek();
558 48
            if (!$this->literal(',')) {
559 48
                break;
560
            }
561
        }
562
563 48
        if ($s) {
564 48
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 557 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
565
        }
566
567 48
        if ($keyName !== null) {
568 47
            unset($this->env->currentProperty);
569
        }
570
571 48
        if (count($values) === 0) {
572 3
            return false;
573
        }
574
575 48
        $value = Compiler::compressList($values, ', ');
576
577 48
        return true;
578
    }
579
580
    /**
581
     * @param $out
582
     *
583
     * @return bool
584
     */
585 48
    protected function parenValue(&$out)
586
    {
587 48
        $s = $this->seek();
588
589
        // speed shortcut
590 48 View Code Duplication
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] !== "(") {
0 ignored issues
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...
591 48
            return false;
592
        }
593
594 5
        $inParens = $this->inParens;
595 5
        if ($this->literal("(") &&
596 5
            ($this->inParens = true) && $this->expression($exp) &&
597 5
            $this->literal(")")
598
        ) {
599 3
            $out = $exp;
600 3
            $this->inParens = $inParens;
601
602 3
            return true;
603
        } else {
604 2
            $this->inParens = $inParens;
605 2
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 587 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
606
        }
607
608 2
        return false;
609
    }
610
611
    /**
612
     * a single value
613
     *
614
     * @param array $value
615
     *
616
     * @return bool
617
     */
618 48
    protected function value(&$value)
619
    {
620 48
        $s = $this->seek();
621
622
        // speed shortcut
623 48
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === "-") {
624
            // negation
625 7
            if ($this->literal("-", false) &&
626 7
                (($this->variable($inner) && $inner = ["variable", $inner]) ||
627 6
                    $this->unit($inner) ||
628 7
                    $this->parenValue($inner))
629
            ) {
630 6
                $value = ["unary", "-", $inner];
631
632 6
                return true;
633
            } else {
634 2
                $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 620 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
635
            }
636
        }
637
638 48
        if ($this->parenValue($value)) {
639 3
            return true;
640
        }
641 48
        if ($this->unit($value)) {
642 38
            return true;
643
        }
644 48
        if ($this->color($value)) {
645 13
            return true;
646
        }
647 48
        if ($this->func($value)) {
648 25
            return true;
649
        }
650 48
        if ($this->stringValue($value)) {
651 21
            return true;
652
        }
653
654 48
        if ($this->keyword($word)) {
655 35
            $value = ['keyword', $word];
656
657 35
            return true;
658
        }
659
660
        // try a variable
661 48
        if ($this->variable($var)) {
662 29
            $value = ['variable', $var];
663
664 29
            return true;
665
        }
666
667
        // unquote string (should this work on any type?
668 48
        if ($this->literal("~") && $this->stringValue($str)) {
669 4
            $value = ["escape", $str];
670
671 4
            return true;
672
        } else {
673 48
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 620 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
674
        }
675
676
        // css hack: \0
677 48
        if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
678 1
            $value = ['keyword', '\\' . $m[1]];
679
680 1
            return true;
681
        } else {
682 48
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 620 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
683
        }
684
685 48
        return false;
686
    }
687
688
    /**
689
     * an import statement
690
     *
691
     * @param array $out
692
     *
693
     * @return bool|null
694
     */
695 49
    protected function import(&$out)
696
    {
697 49
        if (!$this->literal('@import')) {
698 49
            return false;
699
        }
700
701
        // @import "something.css" media;
702
        // @import url("something.css") media;
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% 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...
703
        // @import url(something.css) media;
704
705 3
        if ($this->propertyValue($value)) {
706 3
            $out = ["import", $value];
707
708 3
            return true;
709
        }
710
711
        return false;
712
    }
713
714
    /**
715
     * @param $out
716
     *
717
     * @return bool
718
     */
719 3
    protected function mediaQueryList(&$out)
720
    {
721 3
        if ($this->genericList($list, "mediaQuery", ",", false)) {
722 3
            $out = $list[2];
723
724 3
            return true;
725
        }
726
727
        return false;
728
    }
729
730
    /**
731
     * @param $out
732
     *
733
     * @return bool
734
     */
735 3
    protected function mediaQuery(&$out)
736
    {
737 3
        $s = $this->seek();
738
739 3
        $expressions = null;
740 3
        $parts = [];
741
742 3
        if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) &&
743 3
            $this->keyword($mediaType)
744
        ) {
745 3
            $prop = ["mediaType"];
746 3
            if (isset($only)) {
747
                $prop[] = "only";
748
            }
749 3
            if (isset($not)) {
750
                $prop[] = "not";
751
            }
752 3
            $prop[] = $mediaType;
753 3
            $parts[] = $prop;
754
        } else {
755 1
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 737 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
756
        }
757
758
759 3
        if (!empty($mediaType) && !$this->literal("and")) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
760
            // ~
761
        } else {
762 1
            $this->genericList($expressions, "mediaExpression", "and", false);
763 1
            if (is_array($expressions)) {
764 1
                $parts = array_merge($parts, $expressions[2]);
765
            }
766
        }
767
768 3
        if (count($parts) === 0) {
769
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 737 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
770
771
            return false;
772
        }
773
774 3
        $out = $parts;
775
776 3
        return true;
777
    }
778
779
    /**
780
     * @param $out
781
     *
782
     * @return bool
783
     */
784 1
    protected function mediaExpression(&$out)
785
    {
786 1
        $s = $this->seek();
787 1
        $value = null;
788 1
        if ($this->literal("(") &&
789 1
            $this->keyword($feature) &&
790 1
            ($this->literal(":") && $this->expression($value) || true) &&
791 1
            $this->literal(")")
792
        ) {
793 1
            $out = ["mediaExp", $feature];
794 1
            if ($value) {
795 1
                $out[] = $value;
796
            }
797
798 1
            return true;
799 1
        } elseif ($this->variable($variable)) {
800 1
            $out = ['variable', $variable];
801
802 1
            return true;
803
        }
804
805
        $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 786 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
806
807
        return false;
808
    }
809
810
    /**
811
     * an unbounded string stopped by $end
812
     *
813
     * @param      $end
814
     * @param      $out
815
     * @param null $nestingOpen
816
     * @param null $rejectStrs
817
     *
818
     * @return bool
819
     */
820 26
    protected function openString($end, &$out, $nestingOpen = null, $rejectStrs = null)
821
    {
822 26
        $oldWhite = $this->eatWhiteDefault;
823 26
        $this->eatWhiteDefault = false;
824
825 26
        $stop = ["'", '"', "@{", $end];
826 26
        $stop = array_map([Compiler::class, 'pregQuote'], $stop);
827
        // $stop[] = self::$commentMulti;
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% 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...
828
829 26
        if ($rejectStrs !== null) {
830 25
            $stop = array_merge($stop, $rejectStrs);
831
        }
832
833 26
        $patt = '(.*?)(' . implode("|", $stop) . ')';
834
835 26
        $nestingLevel = 0;
836
837 26
        $content = [];
838 26
        while ($this->match($patt, $m, false)) {
839 26
            if (!empty($m[1])) {
840 25
                $content[] = $m[1];
841 25
                if ($nestingOpen) {
842
                    $nestingLevel += substr_count($m[1], $nestingOpen);
843
                }
844
            }
845
846 26
            $tok = $m[2];
847
848 26
            $this->count -= strlen($tok);
849 26
            if ($tok == $end) {
850 14
                if ($nestingLevel === 0) {
851 14
                    break;
852
                } else {
853
                    $nestingLevel--;
854
                }
855
            }
856
857 24
            if (($tok === "'" || $tok === '"') && $this->stringValue($str)) {
858 13
                $content[] = $str;
859 13
                continue;
860
            }
861
862 23
            if ($tok === "@{" && $this->interpolation($inter)) {
863 3
                $content[] = $inter;
864 3
                continue;
865
            }
866
867 23
            if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
868 23
                break;
869
            }
870
871
            $content[] = $tok;
872
            $this->count += strlen($tok);
873
        }
874
875 26
        $this->eatWhiteDefault = $oldWhite;
876
877 26
        if (count($content) === 0) {
878 4
            return false;
879
        }
880
881
        // trim the end
882 25
        if (is_string(end($content))) {
883 25
            $content[count($content) - 1] = rtrim(end($content));
884
        }
885
886 25
        $out = ["string", "", $content];
887
888 25
        return true;
889
    }
890
891
    /**
892
     * @param $out
893
     *
894
     * @return bool
895
     */
896 48
    protected function stringValue(&$out)
897
    {
898 48
        $s = $this->seek();
899 48
        if ($this->literal('"', false)) {
900 22
            $delim = '"';
901 48
        } elseif ($this->literal("'", false)) {
902 12
            $delim = "'";
903
        } else {
904 48
            return false;
905
        }
906
907 24
        $content = [];
908
909
        // look for either ending delim , escape, or string interpolation
910
        $patt = '([^\n]*?)(@\{|\\\\|' .
911 24
            Compiler::pregQuote($delim) . ')';
912
913 24
        $oldWhite = $this->eatWhiteDefault;
914 24
        $this->eatWhiteDefault = false;
915
916 24
        while ($this->match($patt, $m, false)) {
917 24
            $content[] = $m[1];
918 24
            if ($m[2] === "@{") {
919 6
                $this->count -= strlen($m[2]);
920 6
                if ($this->interpolation($inter)) {
921 6
                    $content[] = $inter;
922
                } else {
923 1
                    $this->count += strlen($m[2]);
924 6
                    $content[] = "@{"; // ignore it
925
                }
926 24
            } elseif ($m[2] === '\\') {
927 2
                $content[] = $m[2];
928 2
                if ($this->literal($delim, false)) {
929 2
                    $content[] = $delim;
930
                }
931
            } else {
932 24
                $this->count -= strlen($delim);
933 24
                break; // delim
934
            }
935
        }
936
937 24
        $this->eatWhiteDefault = $oldWhite;
938
939 24
        if ($this->literal($delim)) {
940 24
            $out = ["string", $delim, $content];
941
942 24
            return true;
943
        }
944
945 1
        $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 898 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
946
947 1
        return false;
948
    }
949
950
    /**
951
     * @param $out
952
     *
953
     * @return bool
954
     */
955 19
    protected function interpolation(&$out)
956
    {
957 19
        $oldWhite = $this->eatWhiteDefault;
958 19
        $this->eatWhiteDefault = true;
959
960 19
        $s = $this->seek();
961 19
        if ($this->literal("@{") &&
962 19
            $this->openString("}", $interp, null, ["'", '"', ";"]) &&
963 19
            $this->literal("}", false)
964
        ) {
965 7
            $out = ["interpolate", $interp];
966 7
            $this->eatWhiteDefault = $oldWhite;
967 7
            if ($this->eatWhiteDefault) {
968 1
                $this->whitespace();
969
            }
970
971 7
            return true;
972
        }
973
974 16
        $this->eatWhiteDefault = $oldWhite;
975 16
        $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 960 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
976
977 16
        return false;
978
    }
979
980
    /**
981
     * @param $unit
982
     *
983
     * @return bool
984
     */
985 49
    protected function unit(&$unit)
986
    {
987
        // speed shortcut
988 49
        if (isset($this->buffer[$this->count])) {
989 49
            $char = $this->buffer[$this->count];
990 49
            if (!ctype_digit($char) && $char !== ".") {
991 49
                return false;
992
            }
993
        }
994
995 49
        if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
996 38
            $unit = ["number", $m[1], empty($m[2]) ? "" : $m[2]];
997
998 38
            return true;
999
        }
1000
1001 49
        return false;
1002
    }
1003
1004
1005
    /**
1006
     * a # color
1007
     *
1008
     * @param $out
1009
     *
1010
     * @return bool
1011
     */
1012 48
    protected function color(&$out)
1013
    {
1014 48
        if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
1015 13
            if (strlen($m[1]) > 7) {
1016 1
                $out = ["string", "", [$m[1]]];
1017
            } else {
1018 13
                $out = ["raw_color", $m[1]];
1019
            }
1020
1021 13
            return true;
1022
        }
1023
1024 48
        return false;
1025
    }
1026
1027
    /**
1028
     * consume an argument definition list surrounded by ()
1029
     * each argument is a variable name with optional value
1030
     * or at the end a ... or a variable named followed by ...
1031
     * arguments are separated by , unless a ; is in the list, then ; is the
1032
     * delimiter.
1033
     *
1034
     * @param $args
1035
     * @param $isVararg
1036
     *
1037
     * @return bool
1038
     * @throws \LesserPhp\Exception\GeneralException
1039
     */
1040 45
    protected function argumentDef(&$args, &$isVararg)
1041
    {
1042 45
        $s = $this->seek();
1043 45
        if (!$this->literal('(')) {
1044 45
            return false;
1045
        }
1046
1047 19
        $values = [];
1048 19
        $delim = ",";
1049 19
        $method = "expressionList";
1050
1051 19
        $isVararg = false;
1052 19
        while (true) {
1053 19
            if ($this->literal("...")) {
1054 2
                $isVararg = true;
1055 2
                break;
1056
            }
1057
1058 19
            if ($this->$method($value)) {
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you mean $values?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1059 16
                if ($value[0] === "variable") {
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you mean $values?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1060 16
                    $arg = ["arg", $value[1]];
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you mean $values?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1061 16
                    $ss = $this->seek();
1062
1063 16
                    if ($this->assign() && $this->$method($rhs)) {
1064 9
                        $arg[] = $rhs;
0 ignored issues
show
Bug introduced by
The variable $rhs does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
1065
                    } else {
1066 13
                        $this->seek($ss);
0 ignored issues
show
Bug introduced by
It seems like $ss defined by $this->seek() on line 1061 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1067 13
                        if ($this->literal("...")) {
1068 2
                            $arg[0] = "rest";
1069 2
                            $isVararg = true;
1070
                        }
1071
                    }
1072
1073 16
                    $values[] = $arg;
1074 16
                    if ($isVararg) {
1075 2
                        break;
1076
                    }
1077 16
                    continue;
1078
                } else {
1079 15
                    $values[] = ["lit", $value];
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you mean $values?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1080
                }
1081
            }
1082
1083
1084 19
            if (!$this->literal($delim)) {
1085 19
                if ($delim === "," && $this->literal(";")) {
1086
                    // found new delim, convert existing args
1087 2
                    $delim = ";";
1088 2
                    $method = "propertyValue";
1089 2
                    $newArg = null;
1090
1091
                    // transform arg list
1092 2
                    if (isset($values[1])) { // 2 items
1093 2
                        $newList = [];
1094 2
                        foreach ($values as $i => $arg) {
1095 2
                            switch ($arg[0]) {
1096 2
                                case "arg":
1097 2
                                    if ($i) {
1098
                                        throw new GeneralException("Cannot mix ; and , as delimiter types");
1099
                                    }
1100 2
                                    $newList[] = $arg[2];
1101 2
                                    break;
1102 2
                                case "lit":
1103 2
                                    $newList[] = $arg[1];
1104 2
                                    break;
1105
                                case "rest":
1106 2
                                    throw new GeneralException("Unexpected rest before semicolon");
1107
                            }
1108
                        }
1109
1110 2
                        $newList = ["list", ", ", $newList];
1111
1112 2
                        switch ($values[0][0]) {
1113 2
                            case "arg":
1114 2
                                $newArg = ["arg", $values[0][1], $newList];
1115 2
                                break;
1116 1
                            case "lit":
1117 1
                                $newArg = ["lit", $newList];
1118 2
                                break;
1119
                        }
1120
1121 2
                    } elseif ($values) { // 1 item
0 ignored issues
show
Bug Best Practice introduced by
The expression $values of type null[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1122 2
                        $newArg = $values[0];
1123
                    }
1124
1125 2
                    if ($newArg !== null) {
1126 2
                        $values = [$newArg];
1127
                    }
1128
                } else {
1129 19
                    break;
1130
                }
1131
            }
1132
        }
1133
1134 19
        if (!$this->literal(')')) {
1135
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 1042 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1136
1137
            return false;
1138
        }
1139
1140 19
        $args = $values;
1141
1142 19
        return true;
1143
    }
1144
1145
    /**
1146
     * consume a list of tags
1147
     * this accepts a hanging delimiter
1148
     *
1149
     * @param array  $tags
1150
     * @param bool   $simple
1151
     * @param string $delim
1152
     *
1153
     * @return bool
1154
     */
1155 49 View Code Duplication
    protected function tags(&$tags, $simple = false, $delim = ',')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1156
    {
1157 49
        $tags = [];
1158 49
        while ($this->tag($tt, $simple)) {
1159 46
            $tags[] = $tt;
1160 46
            if (!$this->literal($delim)) {
1161 46
                break;
1162
            }
1163
        }
1164
1165 49
        return count($tags) !== 0;
1166
    }
1167
1168
    /**
1169
     * list of tags of specifying mixin path
1170
     * optionally separated by > (lazy, accepts extra >)
1171
     *
1172
     * @param array $tags
1173
     *
1174
     * @return bool
1175
     */
1176 49 View Code Duplication
    protected function mixinTags(&$tags)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1177
    {
1178 49
        $tags = [];
1179 49
        while ($this->tag($tt, true)) {
1180 22
            $tags[] = $tt;
1181 22
            $this->literal(">");
1182
        }
1183
1184 49
        return count($tags) !== 0;
1185
    }
1186
1187
    /**
1188
     * a bracketed value (contained within in a tag definition)
1189
     *
1190
     * @param array $parts
1191
     * @param bool $hasExpression
1192
     *
1193
     * @return bool
1194
     */
1195 49
    protected function tagBracket(&$parts, &$hasExpression)
1196
    {
1197
        // speed shortcut
1198 49 View Code Duplication
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] !== "[") {
0 ignored issues
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...
1199 47
            return false;
1200
        }
1201
1202 49
        $s = $this->seek();
1203
1204 49
        $hasInterpolation = false;
1205
1206 49
        if ($this->literal("[", false)) {
1207 3
            $attrParts = ["["];
1208
            // keyword, string, operator
1209 3
            while (true) {
1210 3
                if ($this->literal("]", false)) {
1211 3
                    $this->count--;
1212 3
                    break; // get out early
1213
                }
1214
1215 3
                if ($this->match('\s+', $m)) {
1216
                    $attrParts[] = " ";
1217
                    continue;
1218
                }
1219 3
                if ($this->stringValue($str)) {
1220
                    // escape parent selector, (yuck)
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% 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...
1221 3
                    foreach ($str[2] as &$chunk) {
1222 3
                        $chunk = str_replace($this->lessc->getParentSelector(), '$&$', $chunk);
1223
                    }
1224
1225 3
                    $attrParts[] = $str;
1226 3
                    $hasInterpolation = true;
1227 3
                    continue;
1228
                }
1229
1230 3
                if ($this->keyword($word)) {
1231 3
                    $attrParts[] = $word;
1232 3
                    continue;
1233
                }
1234
1235 3
                if ($this->interpolation($inter)) {
1236 1
                    $attrParts[] = $inter;
1237 1
                    $hasInterpolation = true;
1238 1
                    continue;
1239
                }
1240
1241
                // operator, handles attr namespace too
1242 3
                if ($this->match('[|-~\$\*\^=]+', $m)) {
1243 3
                    $attrParts[] = $m[0];
1244 3
                    continue;
1245
                }
1246
1247
                break;
1248
            }
1249
1250 3
            if ($this->literal("]", false)) {
1251 3
                $attrParts[] = "]";
1252 3
                foreach ($attrParts as $part) {
1253 3
                    $parts[] = $part;
1254
                }
1255 3
                $hasExpression = $hasExpression || $hasInterpolation;
1256
1257 3
                return true;
1258
            }
1259
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 1202 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1260
        }
1261
1262 49
        $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 1202 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1263
1264 49
        return false;
1265
    }
1266
1267
    /**
1268
     * a space separated list of selectors
1269
     *
1270
     * @param      $tag
1271
     * @param bool $simple
1272
     *
1273
     * @return bool
1274
     */
1275 49
    protected function tag(&$tag, $simple = false)
1276
    {
1277 49
        if ($simple) {
1278 49
            $chars = '^@,:;{}\][>\(\) "\'';
1279
        } else {
1280 49
            $chars = '^@,;{}["\'';
1281
        }
1282
1283 49
        $s = $this->seek();
1284
1285 49
        $hasExpression = false;
1286 49
        $parts = [];
1287 49
        while ($this->tagBracket($parts, $hasExpression)) {
1288
            ;
1289
        }
1290
1291 49
        $oldWhite = $this->eatWhiteDefault;
1292 49
        $this->eatWhiteDefault = false;
1293
1294 49
        while (true) {
1295 49
            if ($this->match('([' . $chars . '0-9][' . $chars . ']*)', $m)) {
1296 46
                $parts[] = $m[1];
1297 46
                if ($simple) {
1298 45
                    break;
1299
                }
1300
1301 46
                while ($this->tagBracket($parts, $hasExpression)) {
1302
                    ;
1303
                }
1304 46
                continue;
1305
            }
1306
1307 49
            if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === "@") {
1308 13
                if ($this->interpolation($interp)) {
1309 2
                    $hasExpression = true;
1310 2
                    $interp[2] = true; // don't unescape
1311 2
                    $parts[] = $interp;
1312 2
                    continue;
1313
                }
1314
1315 12
                if ($this->literal("@")) {
1316 12
                    $parts[] = "@";
1317 12
                    continue;
1318
                }
1319
            }
1320
1321 49
            if ($this->unit($unit)) { // for keyframes
1322 9
                $parts[] = $unit[1];
1323 9
                $parts[] = $unit[2];
1324 9
                continue;
1325
            }
1326
1327 49
            break;
1328
        }
1329
1330 49
        $this->eatWhiteDefault = $oldWhite;
1331 49
        if (!$parts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1332 49
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 1283 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1333
1334 49
            return false;
1335
        }
1336
1337 46
        if ($hasExpression) {
1338 4
            $tag = ["exp", ["string", "", $parts]];
1339
        } else {
1340 46
            $tag = trim(implode($parts));
1341
        }
1342
1343 46
        $this->whitespace();
1344
1345 46
        return true;
1346
    }
1347
1348
    /**
1349
     * a css function
1350
     *
1351
     * @param array $func
1352
     *
1353
     * @return bool
1354
     */
1355 48
    protected function func(&$func)
1356
    {
1357 48
        $s = $this->seek();
1358
1359 48
        if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
1360 25
            $fname = $m[1];
1361
1362 25
            $sPreArgs = $this->seek();
1363
1364 25
            $args = [];
1365 25
            while (true) {
1366 25
                $ss = $this->seek();
1367
                // this ugly nonsense is for ie filter properties
1368 25
                if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
1369 1
                    $args[] = ["string", "", [$name, "=", $value]];
1370
                } else {
1371 24
                    $this->seek($ss);
0 ignored issues
show
Bug introduced by
It seems like $ss defined by $this->seek() on line 1366 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1372 24
                    if ($this->expressionList($value)) {
1373 21
                        $args[] = $value;
1374
                    }
1375
                }
1376
1377 25
                if (!$this->literal(',')) {
1378 25
                    break;
1379
                }
1380
            }
1381 25
            $args = ['list', ',', $args];
1382
1383 25
            if ($this->literal(')')) {
1384 25
                $func = ['function', $fname, $args];
1385
1386 25
                return true;
1387 7
            } elseif ($fname === 'url') {
1388
                // couldn't parse and in url? treat as string
1389 6
                $this->seek($sPreArgs);
0 ignored issues
show
Bug introduced by
It seems like $sPreArgs defined by $this->seek() on line 1362 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1390 6
                if ($this->openString(")", $string) && $this->literal(")")) {
1391 6
                    $func = ['function', $fname, $string];
1392
1393 6
                    return true;
1394
                }
1395
            }
1396
        }
1397
1398 48
        $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 1357 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1399
1400 48
        return false;
1401
    }
1402
1403
    /**
1404
     * consume a less variable
1405
     *
1406
     * @param $name
1407
     *
1408
     * @return bool
1409
     */
1410 49
    protected function variable(&$name)
1411
    {
1412 49
        $s = $this->seek();
1413 49
        if ($this->literal($this->lessc->getVPrefix(), false) &&
1414 49
            ($this->variable($sub) || $this->keyword($name))
1415
        ) {
1416 32
            if (!empty($sub)) {
1417 1
                $name = ['variable', $sub];
1418
            } else {
1419 32
                $name = $this->lessc->getVPrefix() . $name;
1420
            }
1421
1422 32
            return true;
1423
        }
1424
1425 49
        $name = null;
1426 49
        $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 1412 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1427
1428 49
        return false;
1429
    }
1430
1431
    /**
1432
     * Consume an assignment operator
1433
     * Can optionally take a name that will be set to the current property name
1434
     *
1435
     * @param string $name
1436
     *
1437
     * @return bool
1438
     */
1439 48
    protected function assign($name = null)
1440
    {
1441 48
        if ($name !== null) {
1442
            $this->currentProperty = $name;
1443
        }
1444
1445 48
        return $this->literal(':') || $this->literal('=');
1446
    }
1447
1448
    /**
1449
     * consume a keyword
1450
     *
1451
     * @param $word
1452
     *
1453
     * @return bool
1454
     */
1455 49
    protected function keyword(&$word)
1456
    {
1457 49
        if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
1458 48
            $word = $m[1];
1459
1460 48
            return true;
1461
        }
1462
1463 49
        return false;
1464
    }
1465
1466
    /**
1467
     * consume an end of statement delimiter
1468
     *
1469
     * @return bool
1470
     */
1471 48
    protected function end()
1472
    {
1473 48
        if ($this->literal(';', false)) {
1474 48
            return true;
1475 12
        } elseif ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
1476
            // if there is end of file or a closing block next then we don't need a ;
1477 10
            return true;
1478
        }
1479
1480 2
        return false;
1481
    }
1482
1483
    /**
1484
     * @param $guards
1485
     *
1486
     * @return bool
1487
     */
1488 19
    protected function guards(&$guards)
1489
    {
1490 19
        $s = $this->seek();
1491
1492 19
        if (!$this->literal("when")) {
1493 19
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 1490 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1494
1495 19
            return false;
1496
        }
1497
1498 5
        $guards = [];
1499
1500 5
        while ($this->guardGroup($g)) {
1501 5
            $guards[] = $g;
1502 5
            if (!$this->literal(",")) {
1503 5
                break;
1504
            }
1505
        }
1506
1507 5
        if (count($guards) === 0) {
1508
            $guards = null;
1509
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 1490 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1510
1511
            return false;
1512
        }
1513
1514 5
        return true;
1515
    }
1516
1517
    /**
1518
     * a bunch of guards that are and'd together
1519
     *
1520
     * @param $guardGroup
1521
     *
1522
     * @return bool
1523
     */
1524 5
    protected function guardGroup(&$guardGroup)
1525
    {
1526 5
        $s = $this->seek();
1527 5
        $guardGroup = [];
1528 5
        while ($this->guard($guard)) {
1529 5
            $guardGroup[] = $guard;
1530 5
            if (!$this->literal("and")) {
1531 5
                break;
1532
            }
1533
        }
1534
1535 5
        if (count($guardGroup) === 0) {
1536
            $guardGroup = null;
1537
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 1526 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1538
1539
            return false;
1540
        }
1541
1542 5
        return true;
1543
    }
1544
1545
    /**
1546
     * @param $guard
1547
     *
1548
     * @return bool
1549
     */
1550 5
    protected function guard(&$guard)
1551
    {
1552 5
        $s = $this->seek();
1553 5
        $negate = $this->literal("not");
1554
1555 5
        if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
1556 5
            $guard = $exp;
1557 5
            if ($negate) {
1558 1
                $guard = ["negate", $guard];
1559
            }
1560
1561 5
            return true;
1562
        }
1563
1564
        $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 1552 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1565
1566
        return false;
1567
    }
1568
1569
    /* raw parsing functions */
1570
1571
    /**
1572
     * @param string $what
1573
     * @param bool $eatWhitespace
1574
     *
1575
     * @return bool
1576
     */
1577 49
    protected function literal($what, $eatWhitespace = null)
1578
    {
1579 49
        if ($eatWhitespace === null) {
1580 49
            $eatWhitespace = $this->eatWhiteDefault;
1581
        }
1582
1583
        // shortcut on single letter
1584 49
        if (!isset($what[1]) && isset($this->buffer[$this->count])) {
1585 49
            if ($this->buffer[$this->count] === $what) {
1586 49
                if (!$eatWhitespace) {
1587 49
                    $this->count++;
1588
1589 49
                    return true;
1590
                }
1591
                // goes below...
1592
            } else {
1593 49
                return false;
1594
            }
1595
        }
1596
1597 49
        if (!isset(self::$literalCache[$what])) {
1598 11
            self::$literalCache[$what] = Compiler::pregQuote($what);
1599
        }
1600
1601 49
        return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
1602
    }
1603
1604
    /**
1605
     * @param        $out
1606
     * @param string $parseItem
1607
     * @param string $delim
1608
     * @param bool   $flatten
1609
     *
1610
     * @return bool
1611
     */
1612 3
    protected function genericList(&$out, $parseItem, $delim = "", $flatten = true)
1613
    {
1614
        // $parseItem is one of mediaQuery, mediaExpression
1615 3
        $s = $this->seek();
1616 3
        $items = [];
1617 3
        while ($this->$parseItem($value)) {
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
1618 3
            $items[] = $value;
1619 3
            if ($delim) {
1620 3
                if (!$this->literal($delim)) {
1621 3
                    break;
1622
                }
1623
            }
1624
        }
1625
1626 3
        if (count($items) === 0) {
1627
            $this->seek($s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->seek() on line 1615 can also be of type boolean; however, LesserPhp\Parser::seek() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1628
1629
            return false;
1630
        }
1631
1632 3
        if ($flatten && count($items) === 1) {
1633
            $out = $items[0];
1634
        } else {
1635 3
            $out = ["list", $delim, $items];
1636
        }
1637
1638 3
        return true;
1639
    }
1640
1641
    /**
1642
     * advance counter to next occurrence of $what
1643
     * $until - don't include $what in advance
1644
     * $allowNewline, if string, will be used as valid char set
1645
     *
1646
     * @param      $what
1647
     * @param      $out
1648
     * @param bool $until
1649
     * @param bool $allowNewline
1650
     *
1651
     * @return bool
1652
     */
1653
    protected function to($what, &$out, $until = false, $allowNewline = false)
0 ignored issues
show
Unused Code introduced by
The parameter $what is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $out is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $until is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $allowNewline is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1654
    {
1655
        die('this seems not to be used, tests dont break');
0 ignored issues
show
Coding Style Compatibility introduced by
The method to() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
1656
        if (is_string($allowNewline)) {
0 ignored issues
show
Unused Code introduced by
if (is_string($allowNewl...wline ? '.' : '[^ ]'; } does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1657
            $validChars = $allowNewline;
1658
        } else {
1659
            $validChars = $allowNewline ? "." : "[^\n]";
1660
        }
1661
        if (!$this->match('(' . $validChars . '*?)' . Compiler::pregQuote($what), $m, !$until)) {
1662
            return false;
1663
        }
1664
        if ($until) {
1665
            $this->count -= strlen($what);
1666
        } // give back $what
1667
        $out = $m[1];
1668
1669
        return true;
1670
    }
1671
1672
    /**
1673
     * try to match something on head of buffer
1674
     *
1675
     * @param string $regex
1676
     * @param      $out
1677
     * @param bool $eatWhitespace
1678
     *
1679
     * @return bool
1680
     */
1681 49
    protected function match($regex, &$out, $eatWhitespace = null)
1682
    {
1683 49
        if ($eatWhitespace === null) {
1684 49
            $eatWhitespace = $this->eatWhiteDefault;
1685
        }
1686
1687 49
        $r = '/' . $regex . ($eatWhitespace && !$this->writeComments ? '\s*' : '') . '/Ais';
1688 49
        if (preg_match($r, $this->buffer, $out, null, $this->count)) {
1689 49
            $this->count += strlen($out[0]);
1690 49
            if ($eatWhitespace && $this->writeComments) {
1691 1
                $this->whitespace();
1692
            }
1693
1694 49
            return true;
1695
        }
1696
1697 49
        return false;
1698
    }
1699
1700
    /**
1701
     * match some whitespace
1702
     *
1703
     * @return bool
1704
     */
1705 49
    protected function whitespace()
1706
    {
1707 49
        if ($this->writeComments) {
1708 1
            $gotWhite = false;
1709 1
            while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
1710 1
                if (isset($m[1]) && empty($this->seenComments[$this->count])) {
1711 1
                    $this->append(["comment", $m[1]]);
1712 1
                    $this->seenComments[$this->count] = true;
1713
                }
1714 1
                $this->count += strlen($m[0]);
1715 1
                $gotWhite = true;
1716
            }
1717
1718 1
            return $gotWhite;
1719
        }
1720
1721 49
        $this->match("", $m);
1722 49
        return strlen($m[0]) > 0;
1723
    }
1724
1725
    /**
1726
     * match something without consuming it
1727
     *
1728
     * @param string $regex
1729
     * @param array $out
1730
     * @param int $from
1731
     *
1732
     * @return int
1733
     */
1734 24
    protected function peek($regex, &$out = null, $from = null)
1735
    {
1736 24
        if ($from === null) {
1737 20
            $from = $this->count;
1738
        }
1739 24
        $r = '/' . $regex . '/Ais';
1740
1741 24
        return preg_match($r, $this->buffer, $out, null, $from);
1742
    }
1743
1744
    /**
1745
     * seek to a spot in the buffer or return where we are on no argument
1746
     *
1747
     * @param int $where
1748
     *
1749
     * @return bool|int
1750
     */
1751 49
    protected function seek($where = null)
1752
    {
1753 49
        if ($where === null) {
1754 49
            return $this->count;
1755
        } else {
1756 49
            $this->count = $where;
1757
        }
1758
1759 49
        return true;
1760
    }
1761
1762
    /* misc functions */
1763
1764
    /**
1765
     * @param string $msg
1766
     * @param int $count
1767
     *
1768
     * @throws \LesserPhp\Exception\GeneralException
1769
     */
1770 5
    public function throwError($msg = 'parse error', $count = null)
1771
    {
1772 5
        $count = $count === null ? $this->count : $count;
1773
1774 5
        $line = $this->line + substr_count(substr($this->buffer, 0, $count), "\n");
1775
1776 5
        if (!empty($this->sourceName)) {
1777
            $loc = "$this->sourceName on line $line";
1778
        } else {
1779 5
            $loc = "line: $line";
1780
        }
1781
1782
        // TODO this depends on $this->count
1783 5
        if ($this->peek("(.*?)(\n|$)", $m, $count)) {
1784 5
            throw new GeneralException("$msg: failed at `$m[1]` $loc");
1785
        } else {
1786
            throw new GeneralException("$msg: $loc");
1787
        }
1788
    }
1789
1790
    /**
1791
     * @param null $selectors
1792
     * @param null $type
1793
     *
1794
     * @return \stdClass
1795
     */
1796 49
    protected function pushBlock($selectors = null, $type = null)
1797
    {
1798 49
        $b = new \stdClass();
1799 49
        $b->parent = $this->env;
1800
1801 49
        $b->type = $type;
1802 49
        $b->id = self::$nextBlockId++;
1803
1804 49
        $b->isVararg = false; // TODO: kill me from here
1805 49
        $b->tags = $selectors;
1806
1807 49
        $b->props = [];
1808 49
        $b->children = [];
1809
1810
        // add a reference to the parser so
1811
        // we can access the parser to throw errors
1812
        // or retrieve the sourceName of this block.
1813 49
        $b->parser = $this;
1814
1815
        // so we know the position of this block
1816 49
        $b->count = $this->count;
1817
1818 49
        $this->env = $b;
1819
1820 49
        return $b;
1821
    }
1822
1823
    /**
1824
     * push a block that doesn't multiply tags
1825
     *
1826
     * @param $type
1827
     *
1828
     * @return \stdClass
1829
     */
1830 49
    protected function pushSpecialBlock($type)
1831
    {
1832 49
        return $this->pushBlock(null, $type);
1833
    }
1834
1835
    /**
1836
     * append a property to the current block
1837
     *
1838
     * @param      $prop
1839
     * @param  $pos
1840
     */
1841 49
    protected function append($prop, $pos = null)
1842
    {
1843 49
        if ($pos !== null) {
1844 49
            $prop[-1] = $pos;
1845
        }
1846 49
        $this->env->props[] = $prop;
1847 49
    }
1848
1849
    /**
1850
     * pop something off the stack
1851
     *
1852
     * @return mixed
1853
     */
1854 46
    protected function pop()
1855
    {
1856 46
        $old = $this->env;
1857 46
        $this->env = $this->env->parent;
1858
1859 46
        return $old;
1860
    }
1861
1862
    /**
1863
     * remove comments from $text
1864
     * todo: make it work for all functions, not just url
1865
     *
1866
     * @param string $text
1867
     *
1868
     * @return string
1869
     */
1870 49
    protected function removeComments($text)
1871
    {
1872
        $look = [
1873 49
            'url(',
1874
            '//',
1875
            '/*',
1876
            '"',
1877
            "'",
1878
        ];
1879
1880 49
        $out = '';
1881 49
        $min = null;
1882 49
        while (true) {
1883
            // find the next item
1884 49
            foreach ($look as $token) {
1885 49
                $pos = mb_strpos($text, $token);
1886 49
                if ($pos !== false) {
1887 27
                    if ($min === null || $pos < $min[1]) {
1888 49
                        $min = [$token, $pos];
1889
                    }
1890
                }
1891
            }
1892
1893 49
            if ($min === null) {
1894 49
                break;
1895
            }
1896
1897 27
            $count = $min[1];
1898 27
            $skip = 0;
1899 27
            $newlines = 0;
1900 27
            switch ($min[0]) {
1901 27 View Code Duplication
                case 'url(':
0 ignored issues
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...
1902 6
                    if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) {
1903 6
                        $count += mb_strlen($m[0]) - mb_strlen($min[0]);
1904
                    }
1905 6
                    break;
1906 27
                case '"':
1907 22
                case "'":
1908 24
                    if (preg_match('/' . $min[0] . '.*?(?<!\\\\)' . $min[0] . '/', $text, $m, 0, $count)) {
1909 24
                        $count += mb_strlen($m[0]) - 1;
1910
                    }
1911 24
                    break;
1912 18
                case '//':
1913 18
                    $skip = mb_strpos($text, "\n", $count);
1914 18
                    if ($skip === false) {
1915
                        $skip = mb_strlen($text) - $count;
1916
                    } else {
1917 18
                        $skip -= $count;
1918
                    }
1919 18
                    break;
1920 4 View Code Duplication
                case '/*':
0 ignored issues
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...
1921 4
                    if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
1922 4
                        $skip = mb_strlen($m[0]);
1923 4
                        $newlines = mb_substr_count($m[0], "\n");
1924
                    }
1925 4
                    break;
1926
            }
1927
1928 27
            if ($skip === 0) {
1929 24
                $count += mb_strlen($min[0]);
1930
            }
1931
1932 27
            $out .= mb_substr($text, 0, $count) . str_repeat("\n", $newlines);
1933 27
            $text = mb_substr($text, $count + $skip);
1934
1935 27
            $min = null;
1936
        }
1937
1938 49
        return $out . $text;
1939
    }
1940
1941
    /**
1942
     * @param bool $writeComments
1943
     */
1944 49
    public function setWriteComments($writeComments)
1945
    {
1946 49
        $this->writeComments = $writeComments;
1947 49
    }
1948
1949
    /**
1950
     * @param $s
1951
     *
1952
     * @return bool
1953
     */
1954 3
    protected function handleLiteralMedia($s)
1955
    {
1956
        // seriously, this || true is required for this statement to work!?
1957 3
        if (($this->mediaQueryList($mediaQueries) || true) && $this->literal('{')) {
1958 3
            $media = $this->pushSpecialBlock('media');
1959 3
            $media->queries = $mediaQueries === null ? [] : $mediaQueries;
1960
1961 3
            return true;
1962
        } else {
1963
            $this->seek($s);
1964
        }
1965
1966
        return false;
1967
    }
1968
1969
    /**
1970
     * @param string $directiveName
1971
     *
1972
     * @return bool
1973
     */
1974 4 View Code Duplication
    protected function handleDirectiveBlock($directiveName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1975
    {
1976
        // seriously, this || true is required for this statement to work!?
1977 4
        if (($this->openString('{', $directiveValue, null, [';']) || true) && $this->literal('{')) {
1978 4
            $dir = $this->pushSpecialBlock('directive');
1979 4
            $dir->name = $directiveName;
1980 4
            if ($directiveValue !== null) {
1981 2
                $dir->value = $directiveValue;
1982
            }
1983
1984 4
            return true;
1985
        }
1986
1987 1
        return false;
1988
    }
1989
1990
    /**
1991
     * @param string $directiveName
1992
     *
1993
     * @return bool
1994
     */
1995 1
    protected function handleDirectiveLine($directiveName)
1996
    {
1997 1
        if ($this->propertyValue($directiveValue) && $this->end()) {
1998 1
            $this->append(['directive', $directiveName, $directiveValue]);
1999
2000 1
            return true;
2001
        }
2002
2003
        return false;
2004
    }
2005
2006
    /**
2007
     * @param string $directiveName
2008
     *
2009
     * @return bool
2010
     */
2011 24 View Code Duplication
    protected function handleRulesetDefinition($directiveName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2012
    {
2013
        //Ruleset Definition
2014
        // seriously, this || true is required for this statement to work!?
2015 24
        if (($this->openString('{', $directiveValue, null, [';']) || true) && $this->literal('{')) {
2016 1
            $dir = $this->pushBlock($this->fixTags(['@' . $directiveName]));
2017 1
            $dir->name = $directiveName;
2018 1
            if ($directiveValue !== null) {
2019
                $dir->value = $directiveValue;
2020
            }
2021
2022 1
            return true;
2023
        }
2024
2025 23
        return false;
2026
    }
2027
}
2028