Core::toCSS()   F
last analyzed

Complexity

Conditions 11
Paths 960

Size

Total Lines 77
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 77
rs 3.4313
c 0
b 0
f 0
cc 11
eloc 41
nc 960
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the ILess
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace ILess\Parser;
11
12
use ILess\Color;
13
use ILess\Context;
14
use ILess\DebugInfo;
15
use ILess\Exception\CompilerException;
16
use ILess\Exception\ParserException;
17
use ILess\ImportedFile;
18
use ILess\Importer;
19
use ILess\Node;
20
use ILess\Node\AlphaNode;
21
use ILess\Node\AnonymousNode;
22
use ILess\Node\AssignmentNode;
23
use ILess\Node\AttributeNode;
24
use ILess\Node\CallNode;
25
use ILess\Node\ColorNode;
26
use ILess\Node\CombinatorNode;
27
use ILess\Node\CommentNode;
28
use ILess\Node\ConditionNode;
29
use ILess\Node\DetachedRulesetNode;
30
use ILess\Node\DimensionNode;
31
use ILess\Node\DirectiveNode;
32
use ILess\Node\ElementNode;
33
use ILess\Node\ExpressionNode;
34
use ILess\Node\ExtendNode;
35
use ILess\Node\ImportNode;
36
use ILess\Node\JavascriptNode;
37
use ILess\Node\KeywordNode;
38
use ILess\Node\MediaNode;
39
use ILess\Node\MixinCallNode;
40
use ILess\Node\MixinDefinitionNode;
41
use ILess\Node\NegativeNode;
42
use ILess\Node\OperationNode;
43
use ILess\Node\ParenNode;
44
use ILess\Node\QuotedNode;
45
use ILess\Node\RuleNode;
46
use ILess\Node\RulesetCallNode;
47
use ILess\Node\RulesetNode;
48
use ILess\Node\SelectorNode;
49
use ILess\Node\UnicodeDescriptorNode;
50
use ILess\Node\UrlNode;
51
use ILess\Node\ValueNode;
52
use ILess\Node\VariableNode;
53
use ILess\Plugin\PostProcessorInterface;
54
use ILess\Plugin\PreProcessorInterface;
55
use ILess\PluginManager;
56
use ILess\SourceMap\Generator;
57
use ILess\Util;
58
use ILess\Variable;
59
use ILess\Visitor\ImportVisitor;
60
use ILess\Visitor\JoinSelectorVisitor;
61
use ILess\Visitor\ProcessExtendsVisitor;
62
use ILess\Visitor\ToCSSVisitor;
63
use ILess\Visitor\Visitor;
64
use InvalidArgumentException;
65
66
/**
67
 * Parser core.
68
 */
69
class Core
70
{
71
    /**
72
     * Parser version.
73
     */
74
    const VERSION = '2.2.0';
75
76
    /**
77
     * Less.js compatibility version.
78
     */
79
    const LESS_JS_VERSION = '2.5.x';
80
81
    /**
82
     * The context.
83
     *
84
     * @var Context
85
     */
86
    protected $context;
87
88
    /**
89
     * The importer.
90
     *
91
     * @var Importer
92
     */
93
    protected $importer;
94
95
    /**
96
     * Array of variables.
97
     *
98
     * @var array
99
     */
100
    protected $variables = [];
101
102
    /**
103
     * Array of parsed rules.
104
     *
105
     * @var array
106
     */
107
    protected $rules = [];
108
109
    /**
110
     * @var ParserInput
111
     */
112
    protected $input;
113
114
    /**
115
     * @var PluginManager|null
116
     */
117
    protected $pluginManager;
118
119
    /**
120
     * Constructor.
121
     *
122
     * @param Context $context The context
123
     * @param Importer $importer The importer
124
     * @param PluginManager $pluginManager The plugin manager
125
     */
126
    public function __construct(Context $context, Importer $importer, PluginManager $pluginManager = null)
127
    {
128
        $this->context = $context;
129
        $this->importer = $importer;
130
        $this->pluginManager = $pluginManager;
131
        $this->input = new ParserInput();
132
    }
133
134
    /**
135
     * Parse a Less string from a given file.
136
     *
137
     * @throws ParserException
138
     *
139
     * @param string|ImportedFile $file The file to parse (Will be loaded via the importer)
140
     * @param bool $returnRuleset Indicates whether the parsed rules should be wrapped in a ruleset.
141
     *
142
     * @return mixed If $returnRuleset is true, ILess\Parser\Core, ILess\ILess\Node\RulesetNode otherwise
143
     */
144
    public function parseFile($file, $returnRuleset = false)
145
    {
146
        // save the previous information
147
        $previousFileInfo = $this->context->currentFileInfo;
148
149
        if (!($file instanceof ImportedFile)) {
150
            $this->context->setCurrentFile($file);
151
152
            if ($previousFileInfo) {
153
                $this->context->currentFileInfo->reference = $previousFileInfo->reference;
154
            }
155
156
            // try to load it via importer
157
            list(, $file) = $this->importer->import($file, true, $this->context->currentFileInfo);
158
159
            /* @var $file ImportedFile */
160
            $this->context->setCurrentFile($file->getPath());
161
            $this->context->currentFileInfo->importedFile = $file;
162
163
            $ruleset = $file->getRuleset();
164
        } else {
165
            $this->context->setCurrentFile($file->getPath());
166
167
            if ($previousFileInfo) {
168
                $this->context->currentFileInfo->reference = $previousFileInfo->reference;
169
            }
170
171
            $this->context->currentFileInfo->importedFile = $file;
172
173
            $ruleset = $file->getRuleset();
174
            if (!$ruleset) {
175
                $file->setRuleset(
176
                    ($ruleset = new RulesetNode([], $this->parse($file->getContent())))
177
                );
178
            }
179
        }
180
181
        if ($previousFileInfo) {
182
            $this->context->currentFileInfo = $previousFileInfo;
183
        }
184
185
        if ($returnRuleset) {
186
            return $ruleset;
187
        }
188
189
        $this->rules = array_merge($this->rules, $ruleset->rules);
190
191
        return $this;
192
    }
193
194
    /**
195
     * Parses a string.
196
     *
197
     * @param string $string The string to parse
198
     * @param string $filename The filename for reference (will be visible in the source map) or path to a fake file which directory will be used for imports
199
     * @param bool $returnRuleset Return the ruleset?
200
     *
201
     * @return $this
202
     */
203
    public function parseString($string, $filename = '__string_to_parse__', $returnRuleset = false)
204
    {
205
        $string = Util::normalizeString((string) $string);
206
207
        // we need unique key
208
        $key = sprintf('%s[__%s__]', $filename, md5($string));
209
210
        // create a dummy information, since we are not parsing a real file,
211
        // but a string coming from outside
212
        $this->context->setCurrentFile($filename);
213
214
        $importedFile = new ImportedFile($key, $string, time());
215
216
        // save information, so the exceptions can handle errors in the string
217
        // and source map is generated for the string
218
        $this->context->currentFileInfo->importedFile = $importedFile;
219
        $this->importer->setImportedFile($key, $importedFile, $key, $this->context->currentFileInfo);
220
221
        if ($this->context->sourceMap) {
222
            $this->context->setFileContent($key, $string);
223
        }
224
225
        $importedFile->setRuleset(
226
            ($ruleset = new RulesetNode([], $this->parse($string)))
227
        );
228
229
        if ($returnRuleset) {
230
            return $ruleset;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $ruleset; (ILess\Node\RulesetNode) is incompatible with the return type documented by ILess\Parser\Core::parseString of type ILess\Parser\Core.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
231
        }
232
233
        $this->rules = array_merge($this->rules, $ruleset->rules);
234
235
        return $this;
236
    }
237
238
    /**
239
     * Adds variables.
240
     *
241
     * @param array $variables Array of variables
242
     *
243
     * @return $this
244
     */
245
    public function addVariables(array $variables)
246
    {
247
        $this->variables = array_merge($this->variables, $variables);
248
249
        return $this;
250
    }
251
252
    /**
253
     * Clears all assigned variables.
254
     *
255
     * @return $this
256
     */
257
    public function clearVariables()
258
    {
259
        $this->variables = [];
260
261
        return $this;
262
    }
263
264
    /**
265
     * Sets variables.
266
     *
267
     * @param array $variables
268
     *
269
     * @return $this
270
     */
271
    public function setVariables(array $variables)
272
    {
273
        $this->variables = $variables;
274
275
        return $this;
276
    }
277
278
    /**
279
     * Unsets a previously set variable.
280
     *
281
     * @param string|array $variable The variable name(s) to unset as string or an array
282
     *
283
     * @see setVariables, addVariables
284
     *
285
     * @return $this
286
     */
287
    public function unsetVariable($variable)
288
    {
289
        if (!is_array($variable)) {
290
            $variable = [$variable];
291
        }
292
293
        foreach ($variable as $name) {
294
            if (isset($this->variables[$name])) {
295
                unset($this->variables[$name]);
296
            } elseif (isset($this->variables['!' . $name])) {
297
                unset($this->variables['!' . $name]);
298
            }
299
        }
300
301
        return $this;
302
    }
303
304
    /**
305
     * Parse a Less string into nodes.
306
     *
307
     * @param string $string The string to parse
308
     *
309
     * @return array
310
     *
311
     * @throws ParserException If there was an error in parsing the string
312
     */
313
    protected function parse($string)
314
    {
315
        $string = Util::normalizeString($string);
316
317
        if ($this->pluginManager) {
318
            $preProcessors = $this->pluginManager->getPreProcessors();
319
            foreach ($preProcessors as $preProcessor) {
320
                /* @var $preProcessor PreProcessorInterface */
321
                $string = $preProcessor->process($string, [
322
                    'context' => $this->context,
323
                    'file_info' => $this->context->currentFileInfo,
324
                    'importer' => $this->importer,
325
                ]);
326
            }
327
        }
328
329
        $this->input = new ParserInput();
330
        $this->input->start($string);
331
        $rules = $this->parsePrimary();
332
333
        $endInfo = $this->input->end();
334
        $error = null;
335
336
        if (!$endInfo->isFinished) {
337
            $message = $endInfo->furthestPossibleErrorMessage;
338
            if (!$message) {
339
                $message = 'Unrecognised input';
340
                if ($endInfo->furthestChar === '}') {
341
                    $message .= '. Possibly missing opening \'{\'';
342
                } elseif ($endInfo->furthestChar === ')') {
343
                    $message .= '. Possibly missing opening \'(\'';
344
                } elseif ($endInfo->furthestReachedEnd) {
345
                    $message .= '. Possibly missing something';
346
                }
347
            }
348
            $error = new ParserException($message, $endInfo->furthest, $this->context->currentFileInfo);
349
        }
350
351
        if ($error) {
352
            throw $error;
353
        }
354
355
        return $rules;
356
    }
357
358
    /**
359
     * Resets the parser.
360
     *
361
     * @param bool $variables Reset also assigned variables via the API?
362
     *
363
     * @return $this
364
     */
365
    public function reset($variables = true)
366
    {
367
        $this->rules = [];
368
369
        if ($variables) {
370
            $this->clearVariables();
371
        }
372
373
        return $this;
374
    }
375
376
    /**
377
     * Returns the plugin manager.
378
     *
379
     * @return PluginManager|null
380
     */
381
    public function getPluginManager()
382
    {
383
        return $this->pluginManager;
384
    }
385
386
    /**
387
     * Generates unique cache key for given $filename.
388
     *
389
     * @param string $filename
390
     *
391
     * @return string
392
     */
393
    protected function generateCacheKey($filename)
394
    {
395
        return Util::generateCacheKey($filename);
396
    }
397
398
    /**
399
     * Returns the CSS.
400
     *
401
     * @return string
402
     */
403
    public function getCSS()
404
    {
405
        if (!count($this->rules)) {
406
            return '';
407
        }
408
409
        return $this->toCSS($this->getRootRuleset(), $this->variables);
410
    }
411
412
    /**
413
     * Returns root ruleset.
414
     *
415
     * @return RulesetNode
416
     */
417
    protected function getRootRuleset()
418
    {
419
        $root = new RulesetNode([], $this->rules);
420
        $root->root = true;
421
        $root->firstRoot = true;
422
        $root->allowImports = true;
423
424
        return $root;
425
    }
426
427
    /**
428
     * Converts the ruleset to CSS.
429
     *
430
     * @param RulesetNode $ruleset
431
     * @param array $variables
432
     *
433
     * @return string The generated CSS code
434
     *
435
     * @throws
436
     */
437
    protected function toCSS(RulesetNode $ruleset, array $variables)
438
    {
439
        $precision = ini_set('precision', 16);
440
        $locale = setlocale(LC_NUMERIC, 0);
441
        setlocale(LC_NUMERIC, 'C');
442
443
        if (extension_loaded('xdebug')) {
444
            $level = ini_set('xdebug.max_nesting_level', PHP_INT_MAX);
445
        }
446
447
        $e = $css = null;
448
        try {
449
            $this->prepareVariables($this->context, $variables);
450
451
            // pre compilation visitors
452
            foreach ($this->getPreCompileVisitors() as $visitor) {
453
                /* @var $visitor Visitor */
454
                $visitor->run($ruleset);
455
            }
456
457
            // compile the ruleset
458
            $compiled = $ruleset->compile($this->context);
459
460
            // post compilation visitors
461
            foreach ($this->getPostCompileVisitors() as $visitor) {
462
                /* @var $visitor Visitor */
463
                $visitor->run($compiled);
464
            }
465
466
            $context = $this->getContext();
467
            $context->numPrecision = 8; // less.js compatibility
468
469
            if ($context->sourceMap) {
470
                $generator = new Generator(
471
                    $compiled,
472
                    $this->context->getContentsMap(), $this->context->sourceMapOptions
473
                );
474
                // will also save file
475
                $css = $generator->generateCSS($this->context);
476
            } else {
477
                $generator = null;
478
                $css = $compiled->toCSS($this->context);
479
            }
480
481
            if ($this->pluginManager) {
482
                // post process
483
                $postProcessors = $this->pluginManager->getPostProcessors();
484
                foreach ($postProcessors as $postProcessor) {
485
                    /* @var $postProcessor PostProcessorInterface */
486
                    $css = $postProcessor->process($css, [
487
                        'context' => $this->context,
488
                        'source_map' => $generator,
489
                        'importer' => $this->importer,
490
                    ]);
491
                }
492
            }
493
494
            if ($this->context->compress) {
495
                $css = preg_replace('/(^(\s)+)|((\s)+$)/', '', $css);
496
            }
497
        } catch (\Exception $e) {
498
        }
499
500
        // restore
501
        setlocale(LC_NUMERIC, $locale);
502
        ini_set('precision', $precision);
503
504
        if (extension_loaded('xdebug')) {
505
            ini_set('xdebug.max_nesting_level', $level);
506
        }
507
508
        if ($e) {
509
            throw $e;
510
        }
511
512
        return $css;
513
    }
514
515
    /**
516
     * Prepare variable to be used as nodes.
517
     *
518
     * @param Context $context
519
     * @param array $variables
520
     */
521
    protected function prepareVariables(Context $context, array $variables)
522
    {
523
        // FIXME: flag to mark variables as prepared!
524
        $prepared = [];
525
        foreach ($variables as $name => $value) {
526
            // user provided node, no need to process it further
527
            if ($value instanceof Node) {
528
                $prepared[] = $value;
529
                continue;
530
            }
531
            // this is not an "real" variable
532
            if (!$value instanceof Variable) {
533
                $value = Variable::create($name, $value);
534
            }
535
            $prepared[] = $value->toNode();
536
        }
537
538
        if (count($prepared)) {
539
            $context->customVariables = new RulesetNode([], $prepared);
540
        }
541
    }
542
543
    /**
544
     * Returns array of pre compilation visitors.
545
     *
546
     * @return array
547
     */
548
    protected function getPreCompileVisitors()
549
    {
550
        $preCompileVisitors = [];
551
552
        if ($this->context->processImports) {
553
            $preCompileVisitors[] = new ImportVisitor($this->getContext(), $this->getImporter());
554
        }
555
556
        if ($this->pluginManager) {
557
            $preCompileVisitors = array_merge(
558
                $preCompileVisitors,
559
                $this->pluginManager->getPreCompileVisitors()
560
            );
561
        }
562
563
        return $preCompileVisitors;
564
    }
565
566
    /**
567
     * Returns an array of post compilation visitors.
568
     *
569
     * @return array
570
     */
571
    protected function getPostCompileVisitors()
572
    {
573
        // core visitors
574
        $postCompileVisitors = [
575
            new JoinSelectorVisitor(),
576
            new ProcessExtendsVisitor(),
577
        ];
578
579
        if ($this->pluginManager) {
580
            $postCompileVisitors = array_merge(
581
                $this->pluginManager->getPostCompileVisitors(),
582
                $postCompileVisitors
583
            );
584
        }
585
586
        $postCompileVisitors[] = new ToCSSVisitor($this->getContext());
587
588
        return $postCompileVisitors;
589
    }
590
591
    /**
592
     * @return array
593
     */
594
    protected function parsePrimary()
595
    {
596
        $root = [];
597
        while (true) {
598
            while (true) {
599
                $node = $this->parseComment();
600
                if (!$node) {
601
                    break;
602
                }
603
                $root[] = $node;
604
            }
605
606
            if ($this->input->finished) {
607
                break;
608
            }
609
610
            if ($this->input->peek('}')) {
611
                break;
612
            }
613
614
            $node = $this->parseExtendRule();
615
            if ($node) {
616
                $root = array_merge($root, $node);
617
                continue;
618
            }
619
620
            $node = $this->matchFuncs(
621
                [
622
                    'parseMixinDefinition',
623
                    'parseRule',
624
                    'parseRuleset',
625
                    'parseMixinCall',
626
                    'parseRulesetCall',
627
                    'parseDirective',
628
                ]
629
            );
630
631
            if ($node) {
632
                $root[] = $node;
633
            } else {
634
                $foundSemiColon = false;
635
                while ($this->input->char(';')) {
636
                    $foundSemiColon = true;
637
                }
638
                if (!$foundSemiColon) {
639
                    break;
640
                }
641
            }
642
        }
643
644
        return $root;
645
    }
646
647
    /**
648
     * comments are collected by the main parsing mechanism and then assigned to nodes
649
     * where the current structure allows it.
650
     *
651
     * @return CommentNode|null
652
     */
653
    protected function parseComment()
654
    {
655
        if (count($this->input->commentStore)) {
656
            $comment = array_shift($this->input->commentStore);
657
658
            return new CommentNode(
659
                $comment['text'],
660
                isset($comment['isLineComment']) ? $comment['isLineComment'] : false,
661
                $comment['index'],
662
                $this->context->currentFileInfo
663
            );
664
        }
665
    }
666
667
    // The variable part of a variable definition. Used in the `rule` parser
668
    //
669
    // @fink();
670
    //
671
    protected function parseRulesetCall()
672
    {
673
        if ($this->input->currentChar() === '@' && ($name = $this->input->re('/\\G(@[\w-]+)\s*\(\s*\)\s*;/'))) {
674
            return new RulesetCallNode($name[1]);
675
        }
676
    }
677
678
    /**
679
     * Parses a mixin definition.
680
     *
681
     * @return MixinDefinitionNode|null
682
     */
683
    protected function parseMixinDefinition()
684
    {
685
        if (($this->input->currentChar() !== '.' && $this->input->currentChar() !== '#') ||
686
            $this->input->peekReg('/\\G^[^{]*\}/')
687
        ) {
688
            return;
689
        }
690
691
        $this->input->save();
692
693
        if ($match = $this->input->re('/\\G([#.](?:[\w-]|\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/')) {
694
            $cond = null;
695
            $name = $match[1];
696
            $argInfo = $this->parseMixinArgs(false);
697
            $params = $argInfo['args'];
698
            $variadic = $argInfo['variadic'];
699
700
            // .mixincall("@{a}");
701
            // looks a bit like a mixin definition..
702
            // also
703
            // .mixincall(@a: {rule: set;});
704
            // so we have to be nice and restore
705
            if (!$this->input->char(')')) {
706
                $this->input->restore("Missing closing ')'");
707
708
                return;
709
            }
710
711
            $this->input->commentStore = [];
712
713
            // Guard
714
            if ($this->input->str('when')) {
715
                $cond = $this->expect('parseConditions', 'Expected conditions');
716
            }
717
718
            $ruleset = $this->parseBlock();
719
            if (is_array($ruleset)) {
720
                $this->input->forget();
721
722
                return new MixinDefinitionNode($name, $params, $ruleset, $cond, $variadic);
0 ignored issues
show
Bug introduced by
It seems like $cond defined by $this->expect('parseCond... 'Expected conditions') on line 715 can also be of type boolean or object; however, ILess\Node\MixinDefinitionNode::__construct() does only seem to accept object<ILess\Node\ConditionNode>|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...
723
            } else {
724
                $this->input->restore();
725
            }
726
        } else {
727
            $this->input->forget();
728
        }
729
    }
730
731
    /**
732
     * Parses a mixin call with an optional argument list.
733
     *
734
     *   #mixins > .square(#fff);
735
     *    .rounded(4px, black);
736
     *   .button;
737
     *
738
     * The `while` loop is there because mixins can be
739
     * namespaced, but we only support the child and descendant
740
     * selector for now.
741
     *
742
     * @return MixinCallNode|null
743
     */
744
    protected function parseMixinCall()
745
    {
746
        $s = $this->input->currentChar();
747
        $important = false;
748
        $index = $this->input->i;
749
        $c = null;
750
        $args = [];
751
752
        if ($s !== '.' && $s !== '#') {
753
            return;
754
        }
755
756
        $this->input->save(); // stop us absorbing part of an invalid selector
757
758
        $elements = [];
759
        while (true) {
760
            $elemIndex = $this->input->i;
761
            $e = $this->input->re('/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/');
762
            if (!$e) {
763
                break;
764
            }
765
            $elements[] = new ElementNode($c, $e, $elemIndex, $this->context->currentFileInfo);
0 ignored issues
show
Bug introduced by

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...
766
            $c = $this->input->char('>');
767
        }
768
769
        if ($elements) {
770
            if ($this->input->char('(')) {
771
                $args = $this->parseMixinArgs(true);
772
                $args = $args['args'];
773
                $this->expect(')');
774
            }
775
776
            if ($this->parseImportant()) {
777
                $important = true;
778
            }
779
780
            if ($this->parseEnd()) {
781
                $this->input->forget();
782
783
                return new MixinCallNode($elements, $args, $index, $this->context->currentFileInfo, $important);
784
            }
785
        }
786
787
        $this->input->restore();
788
    }
789
790
    /**
791
     * Parses mixin arguments.
792
     *
793
     * @param bool $isCall The definition or function call?
794
     *
795
     * @return array
796
     *
797
     * @throws CompilerException If there is an error the definition of arguments
798
     */
799
    protected function parseMixinArgs($isCall)
800
    {
801
        $expressions = [];
802
        $argsSemiColon = [];
803
        $isSemiColonSeparated = null;
804
        $argsComma = [];
805
        $expressionContainsNamed = null;
806
        $name = null;
807
        $expand = null;
808
        $returner = ['args' => null, 'variadic' => false];
809
810
        $this->input->save();
811
812
        while (true) {
813
            if ($isCall) {
814
                $arg = $this->matchFuncs(['parseDetachedRuleset', 'parseExpression']);
815
            } else {
816
                $this->input->commentStore = [];
817
                if ($this->input->str('...')) {
818
                    $returner['variadic'] = true;
819
                    if ($this->input->char(';') && !$isSemiColonSeparated) {
820
                        $isSemiColonSeparated = true;
821
                    }
822
823
                    if ($isSemiColonSeparated) {
824
                        $argsSemiColon[] = ['variadic' => true];
825
                    } else {
826
                        $argsComma[] = ['variadic' => true];
827
                    }
828
                    break;
829
                }
830
                $arg = $this->matchFuncs(
831
                    ['parseEntitiesVariable', 'parseEntitiesLiteral', 'parseEntitiesKeyword']
832
                );
833
            }
834
835
            if (!$arg) {
836
                break;
837
            }
838
839
            $nameLoop = null;
840
            if ($arg instanceof ExpressionNode) {
841
                $arg->throwAwayComments();
842
            }
843
844
            $value = $arg;
845
            $val = null;
846
847
            if ($isCall) {
848
                // ILess\Variable
849
                if (count($arg->value) == 1) {
850
                    $val = $arg->value[0];
851
                }
852
            } else {
853
                $val = $arg;
854
            }
855
856
            if ($val instanceof VariableNode) {
857
                if ($this->input->char(':')) {
858
                    if (count($expressions) > 0) {
859
                        if ($isSemiColonSeparated) {
860
                            throw new CompilerException(
861
                                'Cannot mix ; and , as delimiter types',
862
                                $this->input->i,
863
                                $this->context->currentFileInfo
864
                            );
865
                        }
866
                        $expressionContainsNamed = true;
867
                    }
868
869
                    $value = $this->matchFuncs(['parseDetachedRuleset', 'parseExpression']);
870
                    if (!$value) {
871
                        if ($isCall) {
872
                            throw new CompilerException(
873
                                'Could not understand value for named argument',
874
                                $this->input->i,
875
                                $this->context->currentFileInfo
876
                            );
877
                        } else {
878
                            $this->input->restore();
879
                            $returner['args'] = [];
880
881
                            return $returner;
882
                        }
883
                    }
884
885
                    $nameLoop = ($name = $val->name);
886
                } elseif ($this->input->str('...')) {
887
                    if (!$isCall) {
888
                        $returner['variadic'] = true;
889
890
                        if ($this->input->char(';') && !$isSemiColonSeparated) {
891
                            $isSemiColonSeparated = true;
892
                        }
893
894
                        if ($isSemiColonSeparated) {
895
                            $argsSemiColon[] = ['name' => $arg->name, 'variadic' => true];
896
                        } else {
897
                            $argsComma[] = ['name' => $arg->name, 'variadic' => true];
898
                        }
899
                        break;
900
                    } else {
901
                        $expand = true;
902
                    }
903
                } elseif (!$isCall) {
904
                    $name = $nameLoop = $val->name;
905
                    $value = null;
906
                }
907
            }
908
909
            if ($value) {
910
                $expressions[] = $value;
911
            }
912
913
            $argsComma[] = ['name' => $nameLoop, 'value' => $value, 'expand' => $expand];
914
915
            if ($this->input->char(',')) {
916
                continue;
917
            }
918
919
            if ($this->input->char(';') || $isSemiColonSeparated) {
920
                if ($expressionContainsNamed) {
921
                    throw new CompilerException(
922
                        'Cannot mix ; and , as delimiter types',
923
                        $this->input->i,
924
                        $this->context->currentFileInfo
925
                    );
926
                }
927
928
                $isSemiColonSeparated = true;
929
                if (count($expressions) > 1) {
930
                    $value = new ValueNode($expressions);
931
                }
932
                $argsSemiColon[] = ['name' => $name, 'value' => $value, 'expand' => $expand];
933
                $name = null;
934
                $expressions = [];
935
                $expressionContainsNamed = false;
936
            }
937
        }
938
939
        $this->input->forget();
940
        $returner['args'] = ($isSemiColonSeparated ? $argsSemiColon : $argsComma);
941
942
        return $returner;
943
    }
944
945
    /**
946
     * Parses a rule.
947
     *
948
     * @param bool $tryAnonymous
949
     *
950
     * @return RuleNode|null
951
     */
952
    protected function parseRule($tryAnonymous = false)
953
    {
954
        $merge = null;
955
        $startOfRule = $this->input->i;
956
        $value = null;
957
        $merge = null;
958
        $important = null;
959
        $c = $this->input->currentChar();
960
961
        if ($c === '.' || $c === '#' || $c === '&' || $c === ':') {
962
            return;
963
        }
964
965
        $this->input->save();
966
967
        if ($name = $this->matchFuncs(['parseVariable', 'parseRuleProperty'])) {
968
            $isVariable = is_string($name);
969
            if ($isVariable) {
970
                $value = $this->parseDetachedRuleset();
971
            }
972
973
            $this->input->commentStore = [];
974
975
            if (!$value) {
976
                if (is_array($name) && count($name) > 1) {
977
                    $tmp = array_pop($name);
978
                    $merge = !$isVariable && $tmp->value ? $tmp->value : false;
979
                }
980
981
                $tryValueFirst = !$tryAnonymous && ($this->context->compress || $isVariable);
982
983
                if ($tryValueFirst) {
984
                    $value = $this->parseValue();
985
                }
986
987
                if (!$value) {
988
                    $value = $this->parseAnonymousValue();
989
                    if ($value) {
990
                        $this->input->forget();
991
992
                        return new RuleNode(
993
                            $name,
994
                            $value,
995
                            false,
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
996
                            $merge,
997
                            $startOfRule,
998
                            $this->context->currentFileInfo
999
                        );
1000
                    }
1001
                }
1002
1003
                if (!$tryValueFirst && !$value) {
1004
                    $value = $this->parseValue();
1005
                }
1006
1007
                $important = $this->parseImportant();
1008
            }
1009
1010
            if ($value && $this->parseEnd()) {
1011
                $this->input->forget();
1012
1013
                return new RuleNode(
1014
                    $name, $value, $important, $merge, $startOfRule, $this->context->currentFileInfo
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type object<ILess\Node\DetachedRulesetNode>; however, ILess\Node\RuleNode::__construct() does only seem to accept string|object<ILess\Node\ValueNode>, 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...
Bug introduced by
It seems like $important defined by $this->parseImportant() on line 1007 can also be of type array<integer,string>; however, ILess\Node\RuleNode::__construct() does only seem to accept string|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...
1015
                );
1016
            } else {
1017
                $this->input->restore();
1018
                if ($value && !$tryAnonymous) {
1019
                    return $this->parseRule(true);
1020
                }
1021
            }
1022
        }
1023
    }
1024
1025
    /**
1026
     * Parses an anonymous value.
1027
     *
1028
     * @return AnonymousNode|null
1029
     */
1030
    protected function parseAnonymousValue()
1031
    {
1032
        if ($match = $this->input->re('/\\G([^@+\/\'"*`(;{}-]*);/')) {
1033
            return new AnonymousNode($match[1]);
1034
        }
1035
    }
1036
1037
    /**
1038
     * Parses a ruleset like: `div, .class, body > p {...}`.
1039
     *
1040
     * @return RulesetNode|null
1041
     *
1042
     * @throws ParserException
1043
     */
1044
    protected function parseRuleset()
1045
    {
1046
        $selectors = [];
1047
1048
        $this->input->save();
1049
1050
        $debugInfo = null;
1051
        if ($this->context->dumpLineNumbers) {
1052
            $debugInfo = $this->getDebugInfo($this->input->i);
1053
        }
1054
1055
        while (true) {
1056
            $s = $this->parseLessSelector();
1057
            if (!$s) {
1058
                break;
1059
            }
1060
            $selectors[] = $s;
1061
            $this->input->commentStore = [];
1062
            if ($s->condition && count($selectors) > 1) {
1063
                throw new ParserException(
1064
                    'Guards are only currently allowed on a single selector.',
1065
                    $this->input->i,
1066
                    $this->context->currentFileInfo
1067
                );
1068
            }
1069
1070
            if (!$this->input->char(',')) {
1071
                break;
1072
            }
1073
1074
            if ($s->condition) {
1075
                throw new ParserException(
1076
                    'Guards are only currently allowed on a single selector.',
1077
                    $this->input->i,
1078
                    $this->context->currentFileInfo
1079
                );
1080
            }
1081
1082
            $this->input->commentStore = [];
1083
        }
1084
1085
        if ($selectors && is_array($rules = $this->parseBlock())) {
1086
            $this->input->forget();
1087
            $ruleset = new RulesetNode($selectors, $rules, $this->context->strictImports);
1088
            if ($debugInfo) {
1089
                $ruleset->debugInfo = $debugInfo;
1090
            }
1091
1092
            return $ruleset;
1093
        } else {
1094
            $this->input->restore();
1095
        }
1096
    }
1097
1098
    /**
1099
     * Parses a selector with less extensions e.g. the ability to extend and guard.
1100
     *
1101
     * @return SelectorNode|null
1102
     */
1103
    protected function parseLessSelector()
1104
    {
1105
        return $this->parseSelector(true);
1106
    }
1107
1108
    /**
1109
     * Parses a CSS selector.
1110
     *
1111
     * @param bool $isLess Is this a less sector? (ie. has ability to extend and guard)
1112
     *
1113
     * @return SelectorNode|null
1114
     *
1115
     * @throws ParserException
1116
     */
1117
    protected function parseSelector($isLess = false)
1118
    {
1119
        $elements = [];
1120
        $extendList = [];
1121
        $allExtends = [];
1122
        $condition = null;
1123
        $when = false;
1124
        $e = null;
1125
        $c = null;
1126
        $index = $this->input->i;
1127
1128
        while (($isLess && ($extendList = $this->parseExtend()))
1129
            || ($isLess && ($when = $this->input->str('when'))) || ($e = $this->parseElement())) {
1130
            if ($when) {
1131
                $condition = $this->expect('parseConditions', 'Expected condition');
1132
            } elseif ($condition) {
1133
                throw new ParserException(
1134
                    'CSS guard can only be used at the end of selector.',
1135
                    $index,
1136
                    $this->context->currentFileInfo
1137
                );
1138
            } elseif ($extendList) {
1139
                $allExtends = array_merge($allExtends, $extendList);
1140
            } else {
1141
                if ($allExtends) {
1142
                    throw new ParserException(
1143
                        'Extend can only be used at the end of selector.',
1144
                        $this->input->i,
1145
                        $this->context->currentFileInfo
1146
                    );
1147
                }
1148
                $c = $this->input->currentChar();
1149
                $elements[] = $e;
1150
                $e = null;
1151
            }
1152
1153
            if ($c === '{' || $c === '}' || $c === ';' || $c === ',' || $c === ')') {
1154
                break;
1155
            }
1156
        }
1157
1158
        if ($elements) {
1159
            return new SelectorNode($elements, $allExtends, $condition, $index, $this->context->currentFileInfo);
0 ignored issues
show
Bug introduced by
It seems like $condition defined by $this->expect('parseCond..., 'Expected condition') on line 1131 can also be of type boolean or object; however, ILess\Node\SelectorNode::__construct() does only seem to accept null|object<ILess\Node\ConditionNode>, 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...
1160
        }
1161
1162
        if ($allExtends) {
1163
            throw new ParserException(
1164
                'Extend must be used to extend a selector, it cannot be used on its own',
1165
                $this->input->i,
1166
                $this->context->currentFileInfo
1167
            );
1168
        }
1169
    }
1170
1171
    /**
1172
     * Parses extend.
1173
     *
1174
     * @param bool $isRule Is is a rule?
1175
     *
1176
     * @return ExtendNode|null
1177
     *
1178
     * @throws CompilerException
1179
     */
1180
    protected function parseExtend($isRule = false)
1181
    {
1182
        $extendList = [];
1183
        $index = $this->input->i;
1184
1185
        if (!$this->input->str($isRule ? '&:extend(' : ':extend(')) {
1186
            return;
1187
        }
1188
1189
        do {
1190
            $option = null;
1191
            $elements = [];
1192
            while (!($option = $this->input->re('/\\G(all)(?=\s*(\)|,))/'))) {
1193
                $e = $this->parseElement();
1194
                if (!$e) {
1195
                    break;
1196
                }
1197
                $elements[] = $e;
1198
            }
1199
1200
            if ($option) {
1201
                $option = $option[1];
1202
            }
1203
1204
            if (!$elements) {
1205
                throw new CompilerException(
1206
                    'Missing target selector for :extend()',
1207
                    $index,
1208
                    $this->context->currentFileInfo
1209
                );
1210
            }
1211
1212
            $extendList[] = new ExtendNode(new SelectorNode($elements), $option, $index);
1213
        } while ($this->input->char(','));
1214
1215
        $this->expect('/\\G\)/');
1216
1217
        if ($isRule) {
1218
            $this->expect('/\\G;/');
1219
        }
1220
1221
        return $extendList;
1222
    }
1223
1224
    /**
1225
     * Parses extend rule.
1226
     *
1227
     * @return ExtendNode|null
1228
     */
1229
    protected function parseExtendRule()
1230
    {
1231
        return $this->parseExtend(true);
1232
    }
1233
1234
    /**
1235
     * Parses a selector element.
1236
     *
1237
     *  * `div`
1238
     *  * `+ h1`
1239
     *  * `#socks`
1240
     *  * `input[type="text"]`
1241
     *
1242
     * Elements are the building blocks for selectors,
1243
     * they are made out of a `combinator` and an element name, such as a tag a class, or `*`.
1244
     *
1245
     * @return ElementNode|null
1246
     */
1247
    protected function parseElement()
1248
    {
1249
        $index = $this->input->i;
1250
1251
        $c = $this->parseCombinator();
1252
1253
        $e = $this->match(
1254
            [
1255
                '/\\G^(?:\d+\.\d+|\d+)%/',
1256
                // http://stackoverflow.com/questions/3665962/regular-expression-error-no-ending-delimiter
1257
                '/\\G^(?:[.#]?|:*)(?:[\w-]|[^\\x{00}-\\x{9f}]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/',
1258
                '*',
1259
                '&',
1260
                'parseAttribute',
1261
                '/\\G^\([^&()@]+\)/',
1262
                '/\\G^[\.#:](?=@)/',
1263
                'parseEntitiesVariableCurly',
1264
            ]
1265
        );
1266
1267
        if (!$e) {
1268
            $this->input->save();
1269
            if ($this->input->char('(')) {
1270
                if (($v = $this->parseSelector()) && $this->input->char(')')) {
1271
                    $e = new ParenNode($v);
1272
                    $this->input->forget();
1273
                } else {
1274
                    $this->input->restore("Missing closing ')'");
1275
                }
1276
            } else {
1277
                $this->input->forget();
1278
            }
1279
        }
1280
1281
        if ($e) {
1282
            return new ElementNode($c, $e, $index, $this->context->currentFileInfo);
0 ignored issues
show
Documentation introduced by
$e is of type boolean|object, but the function expects a string|object<ILess\Node>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
It seems like $c defined by $this->parseCombinator() on line 1251 can be null; however, ILess\Node\ElementNode::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1283
        }
1284
    }
1285
1286
    /**
1287
     * Parses a combinator. Combinators combine elements together, in a selector.
1288
     *
1289
     * Because our parser isn't white-space sensitive, special care
1290
     * has to be taken, when parsing the descendant combinator, ` `,
1291
     * as it's an empty space. We have to check the previous character
1292
     * in the input, to see if it's a ` ` character.
1293
     *
1294
     * @return CombinatorNode|null
1295
     */
1296
    protected function parseCombinator()
1297
    {
1298
        $c = $this->input->currentChar();
1299
1300
        if ($c === '/') {
1301
            $this->input->save();
1302
            $slashedCombinator = $this->input->re('/\\G^\/[a-z]+\//i');
1303
            if ($slashedCombinator) {
1304
                $this->input->forget();
1305
1306
                return new CombinatorNode($slashedCombinator);
0 ignored issues
show
Bug introduced by
It seems like $slashedCombinator defined by $this->input->re('/\\G^\\/[a-z]+\\//i') on line 1302 can also be of type array<integer,string>; however, ILess\Node\CombinatorNode::__construct() does only seem to accept string|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...
1307
            }
1308
            $this->input->restore();
1309
        }
1310
1311
        if ($c === '>' || $c === '+' || $c === '~' || $c === '|' || $c === '^') {
1312
            ++$this->input->i;
1313
            if ($c === '^' && $this->input->currentChar() === '^') {
1314
                $c = '^^';
1315
                ++$this->input->i;
1316
            }
1317
            while ($this->input->isWhitespace()) {
1318
                ++$this->input->i;
1319
            }
1320
1321
            return new CombinatorNode($c);
1322
        } elseif ($this->input->isWhiteSpace(-1)) {
1323
            return new CombinatorNode(' ');
1324
        } else {
1325
            return new CombinatorNode();
1326
        }
1327
    }
1328
1329
    /**
1330
     * Parses an attribute.
1331
     *
1332
     * @return AttributeNode|null
1333
     */
1334
    protected function parseAttribute()
1335
    {
1336
        if (!$this->input->char('[')) {
1337
            return;
1338
        }
1339
1340
        $key = $this->parseEntitiesVariableCurly();
1341
        if (!$key) {
1342
            $key = $this->expect('/\\G(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\\\.)+/');
1343
        }
1344
1345
        $val = null;
1346
        if (($op = $this->input->re('/\\G[|~*$^]?=/'))) {
1347
            $val = $this->match(
1348
                [
1349
                    'parseEntitiesQuoted',
1350
                    '/\\G[0-9]+%/',
1351
                    '/\\G[\w-]+/',
1352
                    'parseEntitiesVariableCurly',
1353
                ]
1354
            );
1355
        }
1356
1357
        $this->expect(']');
1358
1359
        return new AttributeNode($key, $op, $val);
0 ignored issues
show
Documentation introduced by
$key is of type boolean|object, but the function expects a string|object<ILess\Node>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
It seems like $op defined by $this->input->re('/\\G[|~*$^]?=/') on line 1346 can also be of type array<integer,string> or null; however, ILess\Node\AttributeNode::__construct() does only seem to accept string, 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...
Documentation introduced by
$val is of type null|boolean|object, but the function expects a string|object<ILess\Node>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1360
    }
1361
1362
    /**
1363
     * Parses a value - a comma-delimited list of expressions like:.
1364
     *
1365
     * `font-family: Baskerville, Georgia, serif;`
1366
     *
1367
     * @return ValueNode|null
1368
     */
1369
    protected function parseValue()
1370
    {
1371
        $e = null;
1372
        $expressions = [];
1373
        do {
1374
            $e = $this->parseExpression();
1375
            if ($e) {
1376
                $expressions[] = $e;
1377
                if (!$this->input->char(',')) {
1378
                    break;
1379
                }
1380
            }
1381
        } while ($e);
1382
1383
        if (count($expressions) > 0) {
1384
            return new ValueNode($expressions);
1385
        }
1386
    }
1387
1388
    /**
1389
     * Parses the `!important` keyword.
1390
     *
1391
     * @return string|null
1392
     */
1393
    protected function parseImportant()
1394
    {
1395
        if ($this->input->currentChar() === '!') {
1396
            return $this->input->re('/\\G! *important/');
1397
        }
1398
    }
1399
1400
    /**
1401
     * Parses a variable.
1402
     *
1403
     * @return string
1404
     */
1405
    protected function parseVariable()
1406
    {
1407
        if ($this->input->currentChar() == '@' && ($name = $this->input->re('/\\G(@[\w-]+)\s*:/'))) {
1408
            return $name[1];
1409
        }
1410
    }
1411
1412
    /**
1413
     * Parses a variable entity using the protective `{}` like: `@{variable}`.
1414
     *
1415
     * @return VariableNode|null
1416
     */
1417
    protected function parseEntitiesVariableCurly()
1418
    {
1419
        $index = $this->input->i;
1420
        if ($this->input->currentChar() === '@' && ($curly = $this->input->re('/\\G@\{([\w-]+)\}/'))) {
1421
            return new VariableNode('@' . $curly[1], $index, $this->context->currentFileInfo);
1422
        }
1423
    }
1424
1425
    /**
1426
     * Parses rule property.
1427
     *
1428
     * @return array
1429
     */
1430
    protected function parseRuleProperty()
1431
    {
1432
        $this->input->save();
1433
        $index = [];
1434
        $name = [];
1435
1436
        $simpleProperty = $this->input->re('/\\G([_a-zA-Z0-9-]+)\s*:/');
1437
        if ($simpleProperty) {
1438
            $name = new KeywordNode($simpleProperty[1]);
1439
            $this->input->forget();
1440
1441
            return [$name];
1442
        }
1443
1444
        // In PHP 5.3 we cannot use $this in the closure
1445
        $input = $this->input;
1446
        $match = function ($re) use (&$index, &$name, $input) {
1447
            $i = $input->i;
1448
            $chunk = $input->re($re);
1449
            if ($chunk) {
1450
                $index[] = $i;
1451
                $name[] = $chunk[1];
1452
1453
                return count($name);
1454
            }
1455
        };
1456
1457
        $match('/\\G(\*?)/');
1458
1459
        while (true) {
1460
            if (!$match('/\\G((?:[\w-]+)|(?:@\{[\w-]+\}))/')) {
1461
                break;
1462
            }
1463
        }
1464
1465
        if (count($name) > 1 && $match('/\\G((?:\+_|\+)?)\s*:/')) {
1466
            $this->input->forget();
1467
1468
            // at last, we have the complete match now. move forward,
1469
            // convert name particles to tree objects and return:
1470
            if ($name[0] === '') {
1471
                array_shift($name);
1472
                array_shift($index);
1473
            }
1474
1475
            for ($k = 0; $k < count($name); ++$k) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
1476
                $s = $name[$k];
1477
                // intentionally @, the name can be an empty string
1478
                $name[$k] = @$s[0] !== '@' ?
1479
                    new KeywordNode($s) :
1480
                    new VariableNode('@' . substr($s, 2, -1), $index[$k], $this->context->currentFileInfo);
1481
            }
1482
1483
            return $name;
1484
        }
1485
1486
        $this->input->restore();
1487
    }
1488
1489
    /**
1490
     * Parses an addition operation.
1491
     *
1492
     * @return OperationNode|null
1493
     */
1494
    protected function parseAddition()
1495
    {
1496
        $operation = false;
1497
        if ($m = $this->parseMultiplication()) {
1498
            $isSpaced = $this->input->isWhitespace(-1);
1499
            while (true) {
1500
                $op = ($op = $this->input->re('/\\G[-+]\s+/')) ? $op : (!$isSpaced ? ($this->match(
1501
                    ['+', '-']
1502
                )) : false);
1503
                if (!$op) {
1504
                    break;
1505
                }
1506
1507
                $a = $this->parseMultiplication();
1508
                if (!$a) {
1509
                    break;
1510
                }
1511
1512
                $m->parensInOp = true;
1513
                $a->parensInOp = true;
1514
1515
                $operation = new OperationNode($op, [$operation ? $operation : $m, $a], $isSpaced);
1516
                $isSpaced = $this->input->isWhitespace(-1);
1517
            }
1518
1519
            return $operation ? $operation : $m;
1520
        }
1521
    }
1522
1523
    /**
1524
     * Parses multiplication operation.
1525
     *
1526
     * @return OperationNode|null
1527
     */
1528
    protected function parseMultiplication()
1529
    {
1530
        $operation = null;
1531
1532
        if ($m = $this->parseOperand()) {
1533
            $isSpaced = $this->input->isWhitespace(-1);
1534
            while (true) {
1535
                if ($this->input->peek('/\\G\/[*\/]/')) {
1536
                    break;
1537
                }
1538
1539
                $this->input->save();
1540
1541
                $op = $this->match(['/', '*']);
1542
1543
                if (!$op) {
1544
                    $this->input->forget();
1545
                    break;
1546
                }
1547
1548
                $a = $this->parseOperand();
1549
1550
                if (!$a) {
1551
                    $this->input->restore();
1552
                    break;
1553
                }
1554
1555
                $this->input->forget();
1556
1557
                $m->parensInOp = true;
1558
                $a->parensInOp = true;
1559
1560
                $operation = new OperationNode($op, [$operation ? $operation : $m, $a], $isSpaced);
0 ignored issues
show
Documentation introduced by
$op is of type boolean|object, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1561
                $isSpaced = $this->input->isWhitespace(-1);
1562
            }
1563
1564
            return $operation ? $operation : $m;
1565
        }
1566
    }
1567
1568
    /**
1569
     * Parses the conditions.
1570
     *
1571
     * @return ConditionNode|null
1572
     */
1573
    protected function parseConditions()
1574
    {
1575
        $index = $this->input->i;
1576
        $condition = null;
1577
        if ($a = $this->parseCondition()) {
1578
            while (true) {
1579
                if (!$this->input->peekReg('/\\G,\s*(not\s*)?\(/') || !$this->input->char(',')) {
1580
                    break;
1581
                }
1582
                $b = $this->parseCondition();
1583
                if (!$b) {
1584
                    break;
1585
                }
1586
1587
                $condition = new ConditionNode('or', $condition ? $condition : $a, $b, $index);
1588
            }
1589
1590
            return $condition ? $condition : $a;
1591
        }
1592
    }
1593
1594
    /**
1595
     * Parses condition.
1596
     *
1597
     * @return ConditionNode|null
1598
     *
1599
     * @throws ParserException
1600
     */
1601
    protected function parseCondition()
1602
    {
1603
        $index = $this->input->i;
1604
        $negate = false;
1605
1606
        if ($this->input->str('not')) {
1607
            $negate = true;
1608
        }
1609
1610
        $this->expect('(');
1611
        if ($a = ($this->matchFuncs(['parseAddition', 'parseEntitiesKeyword', 'parseEntitiesQuoted']))) {
1612
            $op = null;
1613
            if ($this->input->char('>')) {
1614
                if ($this->input->char('=')) {
1615
                    $op = '>=';
1616
                } else {
1617
                    $op = '>';
1618
                }
1619
            } elseif ($this->input->char('<')) {
1620
                if ($this->input->char('=')) {
1621
                    $op = '<=';
1622
                } else {
1623
                    $op = '<';
1624
                }
1625
            } elseif ($this->input->char('=')) {
1626
                if ($this->input->char('>')) {
1627
                    $op = '=>';
1628
                } elseif ($this->input->char('<')) {
1629
                    $op = '=<';
1630
                } else {
1631
                    $op = '=';
1632
                }
1633
            }
1634
1635
            $c = null;
1636
            if ($op) {
1637
                $b = $this->matchFuncs(['parseAddition', 'parseEntitiesKeyword', 'parseEntitiesQuoted']);
1638
                if ($b) {
1639
                    $c = new ConditionNode($op, $a, $b, $index, $negate);
1640
                } else {
1641
                    throw new ParserException('Unexpected expression', $index, $this->context->currentFileInfo);
1642
                }
1643
            } else {
1644
                $c = new ConditionNode('=', $a, new KeywordNode('true'), $index, $negate);
1645
            }
1646
1647
            $this->expect(')');
1648
1649
            return $this->input->str('and') ? new ConditionNode('and', $c, $this->parseCondition()) : $c;
0 ignored issues
show
Bug introduced by
It seems like $this->parseCondition() can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1650
        }
1651
    }
1652
1653
    /**
1654
     * Parses a sub-expression.
1655
     *
1656
     * @return ExpressionNode|null
1657
     */
1658
    protected function parseSubExpression()
1659
    {
1660
        $this->input->save();
1661
1662
        if ($this->input->char('(')) {
1663
            $a = $this->parseAddition();
1664
            if ($a && $this->input->char(')')) {
1665
                $this->input->forget();
1666
                $e = new ExpressionNode([$a]);
1667
                $e->parens = true;
1668
1669
                return $e;
1670
            }
1671
1672
            $this->input->restore("Expected ')'");
1673
1674
            return;
1675
        }
1676
1677
        $this->input->restore();
1678
    }
1679
1680
    /**
1681
     * Parses an operand. An operand is anything that can be part of an operation,
1682
     * such as a color, or a variable.
1683
     *
1684
     * @return NegativeNode|null
1685
     */
1686
    protected function parseOperand()
1687
    {
1688
        $negate = false;
1689
        if ($this->input->peekReg('/\\G^-[@\(]/')) {
1690
            $negate = $this->input->char('-');
1691
        }
1692
1693
        $o = $this->matchFuncs(
1694
            [
1695
                'parseSubExpression',
1696
                'parseEntitiesDimension',
1697
                'parseEntitiesColor',
1698
                'parseEntitiesVariable',
1699
                'parseEntitiesCall',
1700
            ]
1701
        );
1702
1703
        if ($negate) {
1704
            $o->parensInOp = true;
1705
            $o = new NegativeNode($o);
1706
        }
1707
1708
        return $o;
1709
    }
1710
1711
    /**
1712
     * Parses a block. The `block` rule is used by `ruleset` and `mixin definition`.
1713
     * It's a wrapper around the `primary` rule, with added `{}`.
1714
     *
1715
     * @return array
1716
     */
1717
    protected function parseBlock()
1718
    {
1719
        if ($this->input->char('{') && (is_array($content = $this->parsePrimary())) && $this->input->char('}')) {
1720
            return $content;
1721
        }
1722
    }
1723
1724
    /**
1725
     * @return Node|RulesetNode|null
1726
     */
1727
    protected function parseBlockRuleset()
1728
    {
1729
        $block = $this->parseBlock();
1730
        if (null !== $block) {
1731
            $block = new RulesetNode([], $block);
1732
        }
1733
1734
        return $block;
1735
    }
1736
1737
    /**
1738
     * @return DetachedRulesetNode|null
1739
     */
1740
    protected function parseDetachedRuleset()
1741
    {
1742
        $blockRuleset = $this->parseBlockRuleset();
1743
        if ($blockRuleset) {
1744
            return new DetachedRulesetNode($blockRuleset);
1745
        }
1746
    }
1747
1748
    /**
1749
     * Parses comments.
1750
     *
1751
     * @return array Array of comments
1752
     */
1753
    protected function parseComments()
1754
    {
1755
        $comments = [];
1756
        while ($comment = $this->parseComment()) {
1757
            $comments[] = $comment;
1758
        }
1759
1760
        return $comments;
1761
    }
1762
1763
    /**
1764
     * Parses the CSS directive like:.
1765
     *
1766
     * <pre>
1767
     *
1768
     * @charset "utf-8";
1769
     * </pre>
1770
     *
1771
     * @return DirectiveNode|null
1772
     *
1773
     * @throws ParserException
1774
     */
1775
    protected function parseDirective()
1776
    {
1777
        $hasBlock = true;
1778
        $hasIdentifier = false;
1779
        $hasExpression = false;
1780
        $isRooted = true;
1781
        $rules = null;
1782
        $hasUnknown = null;
1783
        $index = $this->input->i;
1784
1785
        if ($this->input->currentChar() !== '@') {
1786
            return;
1787
        }
1788
1789
        $value = $this->matchFuncs(['parseImport', 'parsePlugin', 'parseMedia']);
1790
1791
        if ($value) {
1792
            return $value;
1793
        }
1794
1795
        $this->input->save();
1796
1797
        $name = $this->input->re('/\\G@[a-z-]+/');
1798
1799
        if (!$name) {
1800
            return;
1801
        }
1802
1803
        $nonVendorSpecificName = $name;
1804
        $pos = strpos($name, '-', 2);
1805
        if ($name[1] == '-' && $pos > 0) {
1806
            $nonVendorSpecificName = '@' . substr($name, $pos + 1);
1807
        }
1808
1809
        switch ($nonVendorSpecificName) {
1810
            /*
1811
            case '@font-face':
1812
            case '@viewport':
1813
            case '@top-left':
1814
            case '@top-left-corner':
1815
            case '@top-center':
1816
            case '@top-right':
1817
            case '@top-right-corner':
1818
            case '@bottom-left':
1819
            case '@bottom-left-corner':
1820
            case '@bottom-center':
1821
            case '@bottom-right':
1822
            case '@bottom-right-corner':
1823
            case '@left-top':
1824
            case '@left-middle':
1825
            case '@left-bottom':
1826
            case '@right-top':
1827
            case '@right-middle':
1828
            case '@right-bottom':
1829
                $hasBlock = true;
1830
                $isRooted = true;
1831
                break;
1832
            */
1833
            case '@counter-style':
1834
                $hasIdentifier = true;
1835
                $hasBlock = true;
1836
                break;
1837
            case '@charset':
1838
                $hasIdentifier = true;
1839
                $hasBlock = false;
1840
                break;
1841
            case '@namespace':
1842
                $hasExpression = true;
1843
                $hasBlock = false;
1844
                break;
1845
            case '@keyframes':
1846
                $hasIdentifier = true;
1847
                break;
1848
            case '@host':
1849
            case '@page':
1850
                $hasUnknown = true;
1851
                break;
1852
            case '@document':
1853
            case '@supports':
1854
                $hasUnknown = true;
1855
                $isRooted = false;
1856
                break;
1857
1858
        }
1859
1860
        $this->input->commentStore = [];
1861
1862
        if ($hasIdentifier) {
1863
            $value = $this->parseEntity();
1864
            if (!$value) {
1865
                throw new ParserException(sprintf('Expected %s identifier', $name));
1866
            }
1867
        } elseif ($hasExpression) {
1868
            $value = $this->parseExpression();
1869
            if (!$value) {
1870
                throw new ParserException(sprintf('Expected %s expression', $name));
1871
            }
1872
        } elseif ($hasUnknown) {
1873
            $value = $this->input->re('/\\G^[^{;]+/');
1874
            $value = trim((string) $value);
1875
            if ($value) {
1876
                $value = new AnonymousNode($value);
1877
            }
1878
        }
1879
1880
        if ($hasBlock) {
1881
            $rules = $this->parseBlockRuleset();
1882
        }
1883
1884
        if ($rules || (!$hasBlock && $value && $this->input->char(';'))) {
1885
            $this->input->forget();
1886
1887
            return new DirectiveNode(
1888
                $name, $value, $rules, $index, $this->context->currentFileInfo,
0 ignored issues
show
Bug introduced by
It seems like $name defined by $this->input->re('/\\G@[a-z-]+/') on line 1797 can also be of type array<integer,string>; however, ILess\Node\DirectiveNode::__construct() does only seem to accept string, 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...
1889
                $this->context->dumpLineNumbers ? $this->getDebugInfo($index) : null,
1890
                false, $isRooted
1891
            );
1892
        }
1893
1894
        $this->input->restore('Directive options not recognised');
1895
    }
1896
1897
    /**
1898
     * Entities are the smallest recognized token, and can be found inside a rule's value.
1899
     *
1900
     * @return Node|null
1901
     */
1902
    protected function parseEntity()
1903
    {
1904
        return $this->matchFuncs(
1905
            [
1906
                'parseComment',
1907
                'parseEntitiesLiteral',
1908
                'parseEntitiesVariable',
1909
                'parseEntitiesUrl',
1910
                'parseEntitiesCall',
1911
                'parseEntitiesKeyword',
1912
                'parseEntitiesJavascript',
1913
            ]
1914
        );
1915
    }
1916
1917
    /**
1918
     * Parse entities literal.
1919
     *
1920
     * @return Node|null
1921
     */
1922
    protected function parseEntitiesLiteral()
1923
    {
1924
        return $this->matchFuncs(
1925
            [
1926
                'parseEntitiesDimension',
1927
                'parseEntitiesColor',
1928
                'parseEntitiesQuoted',
1929
                'parseUnicodeDescriptor',
1930
            ]
1931
        );
1932
    }
1933
1934
    /**
1935
     * Parses an entity variable.
1936
     *
1937
     * @return VariableNode|null
1938
     */
1939
    protected function parseEntitiesVariable()
1940
    {
1941
        $index = $this->input->i;
1942
        if ($this->input->currentChar() === '@' && ($name = $this->input->re('/\\G^@@?[\w-]+/'))) {
1943
            return new VariableNode($name, $index, $this->context->currentFileInfo);
0 ignored issues
show
Bug introduced by
It seems like $name defined by $this->input->re('/\\G^@@?[\\w-]+/') on line 1942 can also be of type array<integer,string>; however, ILess\Node\VariableNode::__construct() does only seem to accept string, 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...
1944
        }
1945
    }
1946
1947
    /**
1948
     * Parse entities dimension (a number and a unit like 0.5em, 95%).
1949
     *
1950
     * @return DimensionNode|null
1951
     */
1952
    protected function parseEntitiesDimension()
1953
    {
1954
        if ($this->input->peekNotNumeric()) {
1955
            return;
1956
        }
1957
1958
        if ($value = $this->input->re('/\\G^([+-]?\d*\.?\d+)(%|[a-z]+)?/i')) {
1959
            return new DimensionNode($value[1], isset($value[2]) ? $value[2] : null);
1960
        }
1961
    }
1962
1963
    /**
1964
     * Parses a hexadecimal color.
1965
     *
1966
     * @return ColorNode
1967
     *
1968
     * @throws ParserException
1969
     */
1970
    protected function parseEntitiesColor()
1971
    {
1972
        // we are more tolerate here than in less.js, which can use regexp input property
1973
        // to get the regular expression input, the regexp includes A-z but the color hex code is only A-F
1974
        if ($this->input->currentChar() === '#' && ($rgb = $this->input->re('/\\G#([A-Za-z0-9]{6}|[A-Za-z0-9]{3})/'))) {
1975
            $colorCandidate = $rgb[1];
1976
            // verify if candidate consists only of allowed HEX characters
1977
            if (!preg_match('/^[A-Fa-f0-9]+$/', $colorCandidate)) {
1978
                throw new ParserException('Invalid HEX color code', $this->input->i,
1979
                    $this->context->currentFileInfo);
1980
            }
1981
1982
            return new ColorNode($colorCandidate, null, '#' . $colorCandidate);
1983
        }
1984
    }
1985
1986
    /**
1987
     * Parses a string, which supports escaping " and '
1988
     * "milky way" 'he\'s the one!'.
1989
     *
1990
     * @return QuotedNode|null
1991
     */
1992
    protected function parseEntitiesQuoted()
1993
    {
1994
        $isEscaped = false;
1995
        $index = $this->input->i;
1996
1997
        $this->input->save();
1998
1999
        if ($this->input->char('~')) {
2000
            $isEscaped = true;
2001
        }
2002
2003
        $str = $this->input->quoted();
2004
2005
        if (!$str) {
2006
            $this->input->restore();
2007
2008
            return;
2009
        }
2010
2011
        $this->input->forget();
2012
2013
        return new QuotedNode(
2014
            $str[0],
2015
            substr($str, 1, strlen($str) - 2),
2016
            $isEscaped,
2017
            $index,
2018
            $this->context->currentFileInfo
2019
        );
2020
    }
2021
2022
    /**
2023
     * Parses an unicode descriptor, as is used in unicode-range U+0?? or U+00A1-00A9.
2024
     *
2025
     * @return UnicodeDescriptorNode|null
2026
     */
2027
    protected function parseUnicodeDescriptor()
2028
    {
2029
        if ($ud = $this->input->re('/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/')) {
2030
            return new UnicodeDescriptorNode($ud[0]);
2031
        }
2032
    }
2033
2034
    /**
2035
     * A catch-all word, such as: `black border-collapse`.
2036
     *
2037
     * @return ColorNode|KeywordNode|null
2038
     */
2039
    protected function parseEntitiesKeyword()
2040
    {
2041
        $k = $this->input->char('%');
2042
        if (!$k) {
2043
            $k = $this->input->re('/\\G[_A-Za-z-][_A-Za-z0-9-]*/');
2044
        }
2045
2046
        if ($k) {
2047
            // detected named color and "transparent" keyword
2048
            if ($color = Color::fromKeyword($k)) {
2049
                return new ColorNode($color);
2050
            } else {
2051
                return new KeywordNode($k);
2052
            }
2053
        }
2054
    }
2055
2056
    /**
2057
     * Parses url() tokens.
2058
     *
2059
     * @return UrlNode|null
2060
     */
2061
    protected function parseEntitiesUrl()
2062
    {
2063
        $index = $this->input->i;
2064
        $this->input->autoCommentAbsorb = false;
2065
2066
        if (!$this->input->str('url(')) {
2067
            $this->input->autoCommentAbsorb = true;
2068
2069
            return;
2070
        }
2071
2072
        $value = $this->match(
2073
            [
2074
                'parseEntitiesQuoted',
2075
                'parseEntitiesVariable',
2076
                '/\\G(?>[^\\(\\)\'"]+|(?<=\\\\)[\\(\\)\'"])+/',
2077
            ]
2078
        );
2079
2080
        $this->input->autoCommentAbsorb = true;
2081
2082
        $this->expect(')');
2083
2084
        return new UrlNode(
2085
            (isset($value->value) || $value instanceof VariableNode) ? $value : new AnonymousNode(
0 ignored issues
show
Documentation introduced by
isset($value->value) || ...usNode((string) $value) is of type boolean|object, but the function expects a object<ILess\Node>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2086
                (string) $value
2087
            ),
2088
            $index,
2089
            $this->context->currentFileInfo
2090
        );
2091
    }
2092
2093
    /**
2094
     * Parses a function call.
2095
     *
2096
     * @return CallNode|null
2097
     */
2098
    protected function parseEntitiesCall()
2099
    {
2100
        if ($this->input->peekReg('/\\G^url\(/i')) {
2101
            return;
2102
        }
2103
2104
        $index = $this->input->i;
2105
2106
        $this->input->save();
2107
2108
        $name = $this->input->re('/\\G([\w-]+|%|progid:[\w\.]+)\(/');
2109
2110
        if (!$name) {
2111
            $this->input->forget();
2112
2113
            return;
2114
        }
2115
2116
        $name = $name[1];
2117
        $nameLC = strtolower($name);
2118
2119
        if ($nameLC === 'alpha') {
2120
            $alpha = $this->parseAlpha();
2121
            if ($alpha) {
2122
                $this->input->forget();
2123
2124
                return $alpha;
2125
            }
2126
        }
2127
2128
        $args = $this->parseEntitiesArguments();
2129
2130
        if (!$this->input->char(')')) {
2131
            $this->input->restore("Could not parse call arguments or missing ')'");
2132
2133
            return;
2134
        }
2135
2136
        $this->input->forget();
2137
2138
        return new CallNode($name, $args, $index, $this->context->currentFileInfo);
2139
    }
2140
2141
    /**
2142
     * Parse a list of arguments.
2143
     *
2144
     * @return array
2145
     */
2146
    protected function parseEntitiesArguments()
2147
    {
2148
        $args = [];
2149
2150
        while (true) {
2151
            $arg = $this->matchFuncs(['parseEntitiesAssignment', 'parseExpression']);
2152
            if (!$arg) {
2153
                break;
2154
            }
2155
            $args[] = $arg;
2156
            if (!$this->input->char(',')) {
2157
                break;
2158
            }
2159
        }
2160
2161
        return $args;
2162
    }
2163
2164
    /**
2165
     * Parses an assignments (argument entities for calls).
2166
     * They are present in ie filter properties as shown below.
2167
     * filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* ).
2168
     *
2169
     * @return AssignmentNode|null
2170
     */
2171
    protected function parseEntitiesAssignment()
2172
    {
2173
        $this->input->save();
2174
        $key = $this->input->re('/\\G\w+(?=\s?=)/i');
2175
        if (!$key) {
2176
            $this->input->restore();
2177
2178
            return;
2179
        }
2180
2181
        if (!$this->input->char('=')) {
2182
            $this->input->restore();
2183
2184
            return;
2185
        }
2186
2187
        $value = $this->parseEntity();
2188
        if ($value) {
2189
            $this->input->forget();
2190
2191
            return new AssignmentNode($key, $value);
0 ignored issues
show
Bug introduced by
It seems like $key defined by $this->input->re('/\\G\\w+(?=\\s?=)/i') on line 2174 can also be of type array<integer,string>; however, ILess\Node\AssignmentNode::__construct() does only seem to accept string, 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...
2192
        } else {
2193
            $this->input->restore();
2194
        }
2195
    }
2196
2197
    /**
2198
     * Parses an expression. Expressions either represent mathematical operations,
2199
     * or white-space delimited entities like: `1px solid black`, `@var * 2`.
2200
     *
2201
     * @return ExpressionNode|null
2202
     */
2203
    protected function parseExpression()
2204
    {
2205
        $entities = [];
2206
        $e = null;
2207
        do {
2208
            $e = $this->parseComment();
2209
            if ($e) {
2210
                $entities[] = $e;
2211
                continue;
2212
            }
2213
2214
            $e = $this->matchFuncs(['parseAddition', 'parseEntity']);
2215
            if ($e) {
2216
                $entities[] = $e;
2217
                // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here
2218
                if (!$this->input->peekReg('/\\G\/[\/*]/')) {
2219
                    $delim = $this->input->char('/');
2220
                    if ($delim) {
2221
                        $entities[] = new AnonymousNode($delim);
2222
                    }
2223
                }
2224
            }
2225
        } while ($e);
2226
2227
        if (count($entities) > 0) {
2228
            return new ExpressionNode($entities);
2229
        }
2230
    }
2231
2232
    /**
2233
     * Parses IE's alpha function `alpha(opacity=88)`.
2234
     *
2235
     * @return AlphaNode|null
2236
     */
2237
    protected function parseAlpha()
2238
    {
2239
        if (!$this->input->re('/\\G^opacity=/i')) {
2240
            return;
2241
        }
2242
2243
        $value = $this->input->re('/\\G^\d+/');
2244
        if ($value === null) {
2245
            $value = $this->parseEntitiesVariable();
2246
            if (!$value) {
2247
                throw new ParserException('Could not parse alpha', $this->input->i, $this->context->currentFileInfo);
2248
            }
2249
        }
2250
2251
        $this->expect(')');
2252
2253
        return new AlphaNode($value);
2254
    }
2255
2256
    /**
2257
     * Parses a javascript code.
2258
     *
2259
     * @return JavascriptNode|null
2260
     */
2261
    protected function parseEntitiesJavascript()
2262
    {
2263
        $index = $this->input->i;
2264
2265
        $this->input->save();
2266
2267
        $escape = $this->input->char('~');
2268
        $jsQuote = $this->input->char('`');
2269
2270
        if (!$jsQuote) {
2271
            $this->input->restore();
2272
2273
            return;
2274
        }
2275
2276
        if ($js = $this->input->re('/\\G^[^`]*`/')) {
2277
            $this->input->forget();
2278
2279
            return new JavascriptNode(
2280
                substr($js, 0, strlen($js) - 1),
2281
                (bool) $escape,
2282
                $index,
2283
                $this->context->currentFileInfo
2284
            );
2285
        } else {
2286
            $this->input->restore('Invalid javascript definition');
2287
        }
2288
    }
2289
2290
    /**
2291
     * Parses a @import directive.
2292
     *
2293
     * @return ImportNode|null
2294
     *
2295
     * @throws ParserException
2296
     */
2297
    protected function parseImport()
2298
    {
2299
        $index = $this->input->i;
2300
        $dir = $this->input->re('/\\G^@import?\s+/');
2301
2302
        if ($dir) {
2303
            $options = $this->parseImportOptions();
2304
            if (!$options) {
2305
                $options = [];
2306
            }
2307
2308
            if (($path = $this->matchFuncs(['parseEntitiesQuoted', 'parseEntitiesUrl']))) {
2309
                $features = $this->parseMediaFeatures();
2310
2311
                if (!$this->input->char(';')) {
2312
                    $this->input->i = $index;
2313
                    throw new ParserException(
2314
                        'Missing semi-colon or unrecognised media features on import',
2315
                        $index,
2316
                        $this->context->currentFileInfo
2317
                    );
2318
                }
2319
2320
                if ($features) {
2321
                    $features = new ValueNode($features);
2322
                }
2323
2324
                return new ImportNode($path, $features, $options, $index, $this->context->currentFileInfo);
0 ignored issues
show
Bug introduced by
It seems like $features defined by $this->parseMediaFeatures() on line 2309 can also be of type array; however, ILess\Node\ImportNode::__construct() does only seem to accept null|object<ILess\Node>, 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...
2325
            } else {
2326
                $this->input->i = $index;
2327
                throw new ParserException('Malformed import statement', $index, $this->context->currentFileInfo);
2328
            }
2329
        }
2330
    }
2331
2332
    /**
2333
     * Parses import options.
2334
     *
2335
     * @return array
2336
     */
2337
    protected function parseImportOptions()
2338
    {
2339
        // list of options, surrounded by parens
2340
        if (!$this->input->char('(')) {
2341
            return;
2342
        }
2343
2344
        $options = [];
2345
2346
        do {
2347
            if ($o = $this->parseImportOption()) {
2348
                $optionName = $o;
2349
                $value = true;
2350
                switch ($optionName) {
2351
                    case 'css':
2352
                        $optionName = 'less';
2353
                        $value = false;
2354
                        break;
2355
                    case 'once':
2356
                        $optionName = 'multiple';
2357
                        $value = false;
2358
                        break;
2359
                }
2360
                $options[$optionName] = $value;
2361
                if (!$this->input->char(',')) {
2362
                    break;
2363
                }
2364
            }
2365
        } while ($o);
2366
2367
        $this->expect(')');
2368
2369
        return $options;
2370
    }
2371
2372
    /**
2373
     * Parses import option.
2374
     *
2375
     * @return string|null
2376
     */
2377
    protected function parseImportOption()
2378
    {
2379
        if (($opt = $this->input->re('/\\G(less|css|multiple|once|inline|reference|optional)/'))) {
2380
            return $opt[1];
2381
        }
2382
    }
2383
2384
    /**
2385
     * Parses media block.
2386
     *
2387
     * @return MediaNode|null
2388
     */
2389
    protected function parseMedia()
2390
    {
2391
        $debugInfo = null;
2392
        if ($this->context->dumpLineNumbers) {
2393
            $debugInfo = $this->getDebugInfo($this->input->i);
2394
        }
2395
2396
        $this->input->save();
2397
2398
        if ($this->input->str('@media')) {
2399
            $features = $this->parseMediaFeatures();
2400
            $rules = $this->parseBlock();
2401
2402
            if (null === $rules) {
2403
                $this->input->restore('Media definitions require block statements after any features');
2404
2405
                return;
2406
            }
2407
2408
            $this->input->forget();
2409
2410
            $media = new MediaNode($rules, $features, $this->input->i, $this->context->currentFileInfo);
0 ignored issues
show
Bug introduced by
It seems like $features defined by $this->parseMediaFeatures() on line 2399 can also be of type null; however, ILess\Node\MediaNode::__construct() does only seem to accept array, 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...
2411
2412
            if ($debugInfo) {
2413
                $media->debugInfo = $debugInfo;
2414
            }
2415
2416
            return $media;
2417
        }
2418
2419
        $this->input->restore();
2420
    }
2421
2422
    /**
2423
     * Parses media features.
2424
     *
2425
     * @return array
2426
     */
2427
    protected function parseMediaFeatures()
2428
    {
2429
        $features = [];
2430
        do {
2431
            if ($e = $this->parseMediaFeature()) {
2432
                $features[] = $e;
2433
                if (!$this->input->char(',')) {
2434
                    break;
2435
                }
2436
            } elseif ($e = $this->parseEntitiesVariable()) {
2437
                $features[] = $e;
2438
                if (!$this->input->char(',')) {
2439
                    break;
2440
                }
2441
            }
2442
        } while ($e);
2443
2444
        return $features ? $features : null;
2445
    }
2446
2447
    /**
2448
     * Parses single media feature.
2449
     *
2450
     * @return ExpressionNode|null
2451
     */
2452
    protected function parseMediaFeature()
2453
    {
2454
        $nodes = [];
2455
        $this->input->save();
2456
2457
        do {
2458
            if ($e = $this->matchFuncs(['parseEntitiesKeyword', 'parseEntitiesVariable'])) {
2459
                $nodes[] = $e;
2460
            } elseif ($this->input->char('(')) {
2461
                $p = $this->parseProperty();
2462
                $e = $this->parseValue();
2463
                if ($this->input->char(')')) {
2464
                    if ($p && $e) {
2465
                        $nodes[] = new ParenNode(
2466
                            new RuleNode($p, $e, null, null, $this->input->i, $this->context->currentFileInfo, true)
2467
                        );
2468
                    } elseif ($e) {
2469
                        $nodes[] = new ParenNode($e);
2470
                    } else {
2471
                        $this->input->restore('Badly formed media feature definition');
2472
2473
                        return;
2474
                    }
2475
                } else {
2476
                    $this->input->restore("Missing closing ')'");
2477
2478
                    return;
2479
                }
2480
            }
2481
        } while ($e);
2482
2483
        $this->input->forget();
2484
2485
        if ($nodes) {
2486
            return new ExpressionNode($nodes);
2487
        }
2488
    }
2489
2490
    /**
2491
     * A @plugin directive, used to import compiler extensions dynamically. `@plugin "lib"`;.
2492
     *
2493
     * @return ImportNode|null
2494
     *
2495
     * @throws ParserException
2496
     */
2497
    protected function parsePlugin()
2498
    {
2499
        $index = $this->input->i;
2500
        $dir = $this->input->re('/\\G^@plugin?\s+/');
2501
        if ($dir) {
2502
            $options = ['plugin' => true];
2503
            if (($path = $this->matchFuncs(['parseEntitiesQuoted', 'parseEntitiesUrl']))) {
2504
                if (!$this->input->char(';')) {
2505
                    $this->input->i = $index;
2506
                    throw new ParserException('Missing semi-colon on plugin');
2507
                }
2508
2509
                return new ImportNode($path, null, $options, $index, $this->context->currentFileInfo);
2510
            } else {
2511
                $this->input->i = $index;
2512
                throw new ParserException('Malformed plugin statement');
2513
            }
2514
        }
2515
    }
2516
2517
    /**
2518
     * Parses the property.
2519
     *
2520
     * @return string|null
2521
     */
2522
    protected function parseProperty()
2523
    {
2524
        if ($name = $this->input->re('/\\G(\*?-?[_a-zA-Z0-9-]+)\s*:/')) {
2525
            return $name[1];
2526
        }
2527
    }
2528
2529
    /**
2530
     * Parses a rule terminator.
2531
     *
2532
     * @return string
2533
     */
2534
    protected function parseEnd()
2535
    {
2536
        return ($end = $this->input->char(';')) ? $end : $this->input->peek('}');
2537
    }
2538
2539
    /**
2540
     * Returns the context.
2541
     *
2542
     * @return Context
2543
     */
2544
    public function getContext()
2545
    {
2546
        return $this->context;
2547
    }
2548
2549
    /**
2550
     * Set the current parser environment.
2551
     *
2552
     * @param Context $context
2553
     *
2554
     * @return $this
2555
     */
2556
    public function setContext(Context $context)
2557
    {
2558
        $this->context = $context;
2559
2560
        return $this;
2561
    }
2562
2563
    /**
2564
     * Returns the importer.
2565
     *
2566
     * @return Importer
2567
     */
2568
    public function getImporter()
2569
    {
2570
        return $this->importer;
2571
    }
2572
2573
    /**
2574
     * Set the importer.
2575
     *
2576
     * @param Importer $importer
2577
     *
2578
     * @return $this
2579
     */
2580
    public function setImporter(Importer $importer)
2581
    {
2582
        $this->importer = $importer;
2583
2584
        return $this;
2585
    }
2586
2587
    /**
2588
     * Parse from a token, regexp or string, and move forward if match.
2589
     *
2590
     * @param array|string $token The token
2591
     *
2592
     * @return null|bool|object
2593
     */
2594
    protected function match($token)
2595
    {
2596
        if (!is_array($token)) {
2597
            $token = [$token];
2598
        }
2599
2600
        foreach ($token as $t) {
2601
            if (strlen($t) === 1) {
2602
                $match = $this->input->char($t);
2603
            } elseif ($t[0] !== '/') {
2604
                // Non-terminal, match using a function call
2605
                $match = $this->$t();
2606
            } else {
2607
                $match = $this->input->re($t);
2608
            }
2609
2610
            if (null !== $match) {
2611
                return $match;
2612
            }
2613
        }
2614
    }
2615
2616
    /**
2617
     * Matches given functions. Returns the result of the first which returns
2618
     * any non null value.
2619
     *
2620
     * @param array $functions The array of functions to call
2621
     *
2622
     * @throws InvalidArgumentException If the function does not exist
2623
     *
2624
     * @return Node|mixed
2625
     */
2626
    protected function matchFuncs(array $functions)
2627
    {
2628
        foreach ($functions as $func) {
2629
            if (!method_exists($this, $func)) {
2630
                throw new InvalidArgumentException(sprintf('The function "%s" does not exist.', $func));
2631
            }
2632
            $match = $this->$func();
2633
            if ($match !== null) {
2634
                return $match;
2635
            }
2636
        }
2637
    }
2638
2639
    /**
2640
     * Expects a string to be present at the current position.
2641
     *
2642
     * @param string $token The single character
2643
     * @param string $message The error message for the exception
2644
     *
2645
     * @return Node|null
2646
     *
2647
     * @throws ParserException If the expected token does not match
2648
     */
2649
    protected function expect($token, $message = null)
2650
    {
2651
        $result = $this->match($token);
2652
        if (!$result) {
2653
            throw new ParserException(
2654
                $message ? $message :
2655
                    sprintf(
2656
                        'Expected \'%s\' got \'%s\' at index %s',
2657
                        $token,
2658
                        $this->input->currentChar(),
2659
                        $this->input->i
2660
                    ),
2661
                $this->input->i, $this->context->currentFileInfo
2662
            );
2663
        }
2664
2665
        return $result;
2666
    }
2667
2668
    /**
2669
     * Returns the debug information.
2670
     *
2671
     * @param int $index The index
2672
     *
2673
     * @return \ILess\DebugInfo
2674
     */
2675
    protected function getDebugInfo($index)
2676
    {
2677
        list($lineNumber) = Util::getLocation($this->input->getInput(), $index);
2678
2679
        return new DebugInfo($this->context->currentFileInfo->filename, $lineNumber);
2680
    }
2681
}
2682