Issues (191)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

lib/ILess/Node/RulesetNode.php (19 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\Node;
11
12
use ILess\Context;
13
use ILess\DefaultFunc;
14
use ILess\Exception\ParserException;
15
use ILess\Node;
16
use ILess\Output\OutputInterface;
17
use ILess\Util\Serializer;
18
use ILess\Visitor\VisitorInterface;
19
20
/**
21
 * Ruleset.
22
 */
23
class RulesetNode extends Node implements MarkableAsReferencedInterface,
24
    MakeableImportantInterface, ConditionMatchableInterface,
25
    ReferencedInterface, \Serializable
26
{
27
    /**
28
     * Ruleset paths like: `#foo #bar`.
29
     *
30
     * @var array
31
     */
32
    public $paths = [];
33
34
    /**
35
     * Strict imports flag.
36
     *
37
     * @var bool
38
     */
39
    public $strictImports = false;
40
41
    /**
42
     * Array of selectors.
43
     *
44
     * @var array
45
     */
46
    public $selectors = [];
47
48
    /**
49
     * Is first root?
50
     *
51
     * @var bool
52
     */
53
    public $firstRoot = false;
54
55
    /**
56
     * Is root?
57
     *
58
     * @var bool
59
     */
60
    public $root = false;
61
62
    /**
63
     * Array of rules.
64
     *
65
     * @var array
66
     */
67
    public $rules;
68
69
    /**
70
     * Allow imports flag.
71
     *
72
     * @var bool
73
     */
74
    public $allowImports = false;
75
76
    /**
77
     * Multi media flag.
78
     *
79
     * @var bool
80
     */
81
    public $multiMedia = false;
82
83
    /**
84
     * Array of extends.
85
     *
86
     * @var array
87
     */
88
    public $allExtends;
89
90
    /**
91
     * Extend on every path flag.
92
     *
93
     * @var bool
94
     */
95
    public $extendOnEveryPath = false;
96
97
    /**
98
     * Original ruleset id.
99
     *
100
     * @var RulesetNode|null
101
     */
102
    public $originalRuleset;
103
104
    /**
105
     * The id.
106
     *
107
     * @var int
108
     */
109
    public $rulesetId;
110
111
    /**
112
     * Node type.
113
     *
114
     * @var string
115
     */
116
    protected $type = 'Ruleset';
117
118
    /**
119
     * Array of lookups.
120
     *
121
     * @var array
122
     */
123
    protected $lookups = [];
124
125
    /**
126
     * Variables cache array.
127
     *
128
     * @var array
129
     *
130
     * @see resetCache
131
     */
132
    protected $variables;
133
134
    /**
135
     * Internal flag. Selectors are referenced.
136
     *
137
     * @var bool
138
     *
139
     * @see markAsReferenced
140
     */
141
    protected $isReferenced = false;
142
143
    /**
144
     * @var \ILess\FunctionRegistry
145
     */
146
    public $functionRegistry;
147
148
    /**
149
     * @var array|null
150
     */
151
    public $functions;
152
153
    /**
154
     * Constructor.
155
     *
156
     * @param array $selectors Array of selectors
157
     * @param array $rules Array of rules
158
     * @param bool $strictImports Strict imports?
159
     */
160
    public function __construct(array $selectors, array $rules, $strictImports = false)
161
    {
162
        $this->selectors = $selectors;
163
        $this->rules = $rules;
164
        $this->strictImports = (boolean) $strictImports;
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170
    public function accept(VisitorInterface $visitor)
171
    {
172
        if ($this->paths) {
173
            $visitor->visitArray($this->paths, true);
174
        } elseif ($this->selectors) {
175
            $this->selectors = $visitor->visitArray($this->selectors);
0 ignored issues
show
Documentation Bug introduced by
It seems like $visitor->visitArray($this->selectors) of type * is incompatible with the declared type array of property $selectors.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
176
        }
177
        if ($this->rules) {
178
            $this->rules = $visitor->visitArray($this->rules);
0 ignored issues
show
Documentation Bug introduced by
It seems like $visitor->visitArray($this->rules) of type * is incompatible with the declared type array of property $rules.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
179
        }
180
    }
181
182
    /**
183
     * Compiles the node.
184
     *
185
     * @param Context $context The context
186
     * @param array|null $arguments Array of arguments
187
     * @param bool|null $important Important flag
188
     *
189
     * @throws ParserException
190
     *
191
     * @return RulesetNode
192
     */
193
    public function compile(Context $context, $arguments = null, $important = null)
194
    {
195
        // compile selectors
196
        $selectors = [];
197
        $hasOnePassingSelector = false;
198
199
        if ($count = count($this->selectors)) {
200
            DefaultFunc::error(new ParserException('it is currently only allowed in parametric mixin guards'));
201
            for ($i = 0; $i < $count; ++$i) {
202
                $selector = $this->selectors[$i]->compile($context);
203
                /* @var $selector SelectorNode */
204
                $selectors[] = $selector;
205
                if ($selector->compiledCondition) {
206
                    $hasOnePassingSelector = true;
207
                }
208
            }
209
            DefaultFunc::reset();
210
        } else {
211
            $hasOnePassingSelector = true;
212
        }
213
214
        $ruleset = new self($selectors, $this->rules, $this->strictImports);
215
        $ruleset->originalRuleset = $this;
216
        $ruleset->root = $this->root;
217
        $ruleset->firstRoot = $this->firstRoot;
218
        $ruleset->allowImports = $this->allowImports;
219
220
        if ($this->debugInfo) {
221
            $ruleset->debugInfo = $this->debugInfo;
222
        }
223
224
        if (!$hasOnePassingSelector) {
225
            $ruleset->rules = [];
226
        }
227
228
        // inherit a function registry from the frames stack when possible;
229
        // otherwise from the global registry
230
        $found = null;
231
        foreach ($context->frames as $i => $frame) {
232
            if ($frame->functionRegistry) {
233
                $found = $frame->functionRegistry;
234
                break;
235
            }
236
        }
237
238
        $registry = $found ? $found : $context->getFunctionRegistry();
239
        $ruleset->functionRegistry = $registry->inherit();
240
241
        // push the current ruleset to the frames stack
242
        $context->unshiftFrame($ruleset);
243
244
        // current selectors
245
        array_unshift($context->selectors, $this->selectors);
246
247
        // Evaluate imports
248
        if ($ruleset->root || $ruleset->allowImports || !$ruleset->strictImports) {
249
            $ruleset->compileImports($context);
250
        }
251
252
        // count after compile imports was called
253
        $rulesetCount = count($ruleset->rules);
254
255
        // Store the frames around mixin definitions,
256
        // so they can be evaluated like closures when the time comes.
257
        foreach ($ruleset->rules as $i => $rule) {
258
            /* @var $rule RuleNode */
259
            if ($rule && $rule->compileFirst()) {
260
                $ruleset->rules[$i] = $rule->compile($context);
261
            }
262
        }
263
264
        $mediaBlockCount = count($context->mediaBlocks);
265
266
        // Evaluate mixin calls.
267
        for ($i = 0; $i < $rulesetCount; ++$i) {
268
            $rule = $ruleset->rules[$i];
269
270
            if ($rule instanceof MixinCallNode) {
271
                $rule = $rule->compile($context);
272
                $temp = [];
273
                foreach ($rule as $r) {
274
                    if (($r instanceof RuleNode) && $r->variable) {
275
                        // do not pollute the scope if the variable is
276
                        // already there. consider returning false here
277
                        // but we need a way to "return" variable from mixins
278
                        if (!$ruleset->variable($r->name)) {
279
                            $temp[] = $r;
280
                        }
281
                    } else {
282
                        $temp[] = $r;
283
                    }
284
                }
285
                $tempCount = count($temp) - 1;
286
                array_splice($ruleset->rules, $i, 1, $temp);
287
                $rulesetCount += $tempCount;
288
                $i += $tempCount;
289
                $ruleset->resetCache();
290
            } elseif ($rule instanceof RulesetCallNode) {
291
                $rule = $rule->compile($context);
292
                $rules = [];
293
                foreach ($rule->rules as $r) {
294
                    if (($r instanceof RuleNode && $r->variable)) {
295
                        continue;
296
                    }
297
                    $rules[] = $r;
298
                }
299
300
                array_splice($ruleset->rules, $i, 1, $rules);
301
                $tempCount = count($rules);
302
                $rulesetCount += $tempCount - 1;
303
                $i += $tempCount - 1;
304
                $ruleset->resetCache();
305
            }
306
        }
307
308
        // Evaluate everything else
309
        for ($i = 0; $i < count($ruleset->rules); ++$i) {
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...
310
            $rule = $ruleset->rules[$i];
311
            /* @var $rule Node */
312
            if ($rule && !$rule->compileFirst()) {
313
                $ruleset->rules[$i] = $rule instanceof CompilableInterface ? $rule->compile($context) : $rule;
314
            }
315
        }
316
317
        // Evaluate everything else
318
        for ($i = 0; $i < count($ruleset->rules); ++$i) {
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...
319
            $rule = $ruleset->rules[$i];
320
            // for rulesets, check if it is a css guard and can be removed
321
            if ($rule instanceof self && count($rule->selectors) === 1) {
322
                // check if it can be folded in (e.g. & where)
323
                if ($rule->selectors[0]->isJustParentSelector()) {
324
                    array_splice($ruleset->rules, $i--, 1);
325
                    for ($j = 0; $j < count($rule->rules); ++$j) {
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...
326
                        $subRule = $rule->rules[$j];
327
                        if (!($subRule instanceof RuleNode) || !$subRule->variable) {
328
                            array_splice($ruleset->rules, ++$i, 0, [$subRule]);
329
                        }
330
                    }
331
                }
332
            }
333
        }
334
335
        // Pop the stack
336
        $context->shiftFrame();
337
        array_shift($context->selectors);
338
339
        if ($mediaBlockCount) {
340
            for ($i = $mediaBlockCount, $count = count($context->mediaBlocks); $i < $count; ++$i) {
341
                $context->mediaBlocks[$i]->bubbleSelectors($selectors);
342
            }
343
        }
344
345
        return $ruleset;
346
    }
347
348
    /**
349
     * Compiles the imports.
350
     *
351
     * @param Context $context
352
     */
353
    public function compileImports(Context $context)
354
    {
355
        for ($i = 0; $i < count($this->rules); ++$i) {
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...
356
            $rule = $this->rules[$i];
357
358
            if (!($rule instanceof ImportNode)) {
359
                continue;
360
            }
361
362
            $importRules = $rule->compile($context);
363
364
            if (is_array($importRules) && count($importRules)) {
365
                array_splice($this->rules, $i, 1, $importRules);
366
                $i += count($importRules) - 1;
367
            } else {
368
                array_splice($this->rules, $i, 1, [$importRules]);
369
            }
370
371
            $this->resetCache();
372
        }
373
    }
374
375
    /**
376
     * Resets the cache for variables and lookups.
377
     */
378
    public function resetCache()
379
    {
380
        $this->variables = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $variables.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
381
        $this->rulesets = [];
382
        $this->lookups = [];
383
    }
384
385
    /**
386
     * Returns the variable by name.
387
     *
388
     * @param string $name
389
     *
390
     * @return RuleNode
391
     */
392
    public function variable($name)
393
    {
394
        $vars = $this->variables();
395
396
        return isset($vars[$name]) ? $vars[$name] : null;
397
    }
398
399
    /**
400
     * Returns an array of variables.
401
     *
402
     * @return array
403
     */
404
    public function variables()
405
    {
406
        if ($this->variables === null) {
407
            $this->variables = [];
408
            foreach ($this->rules as $r) {
409
                if ($r instanceof RuleNode && $r->variable == true) {
410
                    $this->variables[$r->name] = $r;
411
                }
412
                // when evaluating variables in an import statement, imports have not been eval'd
413
                // so we need to go inside import statements.
414
                // guard against root being a string (in the case of inlined less)
415
                if ($r instanceof ImportNode && self::methodExists($r->root, 'variables')) {
416
                    $vars = $r->root->variables();
0 ignored issues
show
It seems like you code against a specific sub-type and not the parent class ILess\Node as the method variables() does only exist in the following sub-classes of ILess\Node: ILess\Node\MixinDefinitionNode, ILess\Node\RulesetNode. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
417
                    foreach ($vars as $name => $value) {
418
                        $this->variables[$name] = $value;
419
                    }
420
                }
421
            }
422
        }
423
424
        return $this->variables;
425
    }
426
427
    /**
428
     * {@inheritdoc}
429
     */
430
    public function generateCSS(Context $context, OutputInterface $output)
431
    {
432
        if (!$this->root) {
433
            ++$context->tabLevel;
434
        }
435
436
        $tabRuleStr = $tabSetStr = '';
437
        if (!$context->compress && $context->tabLevel) {
438
            $tabRuleStr = str_repeat('  ', $context->tabLevel);
439
            $tabSetStr = str_repeat('  ', $context->tabLevel - 1);
440
        }
441
442
        $ruleNodes = $rulesetNodes = $charsetRuleNodes = [];
443
        $charsetNodeIndex = 0;
444
        $importNodeIndex = 0;
445
446
        for ($i = 0; $i < count($this->rules); ++$i) {
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...
447
            $rule = $this->rules[$i];
448
            if ($rule instanceof CommentNode) {
449
                if ($importNodeIndex === $i) {
450
                    ++$importNodeIndex;
451
                }
452
                array_push($ruleNodes, $rule);
453
            } elseif ($rule instanceof DirectiveNode && $rule->isCharset()) {
454
                array_splice($ruleNodes, $charsetNodeIndex, 0, [$rule]);
455
                ++$charsetNodeIndex;
456
                ++$importNodeIndex;
457
            } elseif ($rule instanceof ImportNode) {
458
                array_splice($ruleNodes, $importNodeIndex, 0, [$rule]);
459
                ++$importNodeIndex;
460
            } else {
461
                array_push($ruleNodes, $rule);
462
            }
463
        }
464
465
        $ruleNodes = array_merge($charsetRuleNodes, $ruleNodes);
466
467
        // If this is the root node, we don't render
468
        // a selector, or {}.
469
        if (!$this->root) {
470
            if ($this->debugInfo) {
471
                // debug?
472
                $debugInfo = self::getDebugInfo($context, $this, $tabSetStr);
473
                if ($debugInfo) {
474
                    $output->add($debugInfo)->add($tabSetStr);
475
                }
476
            }
477
478
            $sep = $context->compress ? ',' : (",\n" . $tabSetStr);
479
            for ($i = 0, $count = count($this->paths); $i < $count; ++$i) {
480
                $path = $this->paths[$i];
481
                /* @var $path SelectorNode */
482
                if (!($pathSubCnt = count($path))) {
483
                    continue;
484
                }
485
486
                if ($i > 0) {
487
                    $output->add($sep);
488
                }
489
490
                $context->firstSelector = true;
491
                $path[0]->generateCSS($context, $output);
492
                $context->firstSelector = false;
493
494
                for ($j = 1; $j < $pathSubCnt; ++$j) {
495
                    $path[$j]->generateCSS($context, $output);
496
                }
497
            }
498
499
            $output->add(($context->compress ? '{' : " {\n") . $tabRuleStr);
500
        }
501
502
        // Compile rules and rulesets
503
        for ($i = 0, $ruleNodesCount = count($ruleNodes); $i < $ruleNodesCount; ++$i) {
504
            $rule = $ruleNodes[$i];
505
            /* @var $rule RuleNode */
506
            if ($i + 1 === $ruleNodesCount) {
507
                $context->lastRule = true;
508
            }
509
510
            $currentLastRule = $context->lastRule;
511
512
            if ($rule->isRulesetLike()) {
513
                $context->lastRule = false;
514
            }
515
516
            if ($rule instanceof GenerateCSSInterface) {
517
                $rule->generateCSS($context, $output);
518
            } elseif ($rule->value) {
519
                $output->add((string) $rule->value);
520
            }
521
522
            $context->lastRule = $currentLastRule;
523
524
            if (!$context->lastRule) {
525
                $output->add($context->compress ? '' : ("\n" . $tabRuleStr));
526
            } else {
527
                $context->lastRule = false;
528
            }
529
        }
530
531
        if (!$this->root) {
532
            $output->add($context->compress ? '}' : "\n" . $tabSetStr . '}');
533
            --$context->tabLevel;
534
        }
535
536
        if (!$output->isEmpty() && !$context->compress && $this->firstRoot) {
537
            $output->add("\n");
538
        }
539
    }
540
541
    /**
542
     * Marks as referenced.
543
     */
544
    public function markReferenced()
545
    {
546
        foreach ($this->selectors as $s) {
547
            /* @var $s SelectorNode */
548
            $s->markReferenced();
549
        }
550
551
        foreach ($this->rules as $r) {
552
            if ($r instanceof MarkableAsReferencedInterface) {
553
                $r->markReferenced();
554
            }
555
        }
556
    }
557
558
    /**
559
     * Is referenced?
560
     *
561
     * @return bool
562
     */
563
    public function getIsReferenced()
564
    {
565
        foreach ($this->paths as $path) {
566
            foreach ($path as $p) {
567
                if ($p instanceof ReferencedInterface && $p->getIsReferenced()) {
568
                    return true;
569
                }
570
            }
571
        }
572
573
        foreach ($this->selectors as $selector) {
574
            if ($selector instanceof ReferencedInterface && $selector->getIsReferenced()) {
575
                return true;
576
            }
577
        }
578
579
        return false;
580
    }
581
582
    /**
583
     * Returns ruleset with nodes marked as important.
584
     *
585
     * @return RulesetNode
586
     */
587
    public function makeImportant()
588
    {
589
        $importantRules = [];
590
        foreach ($this->rules as $rule) {
591
            if ($rule instanceof MakeableImportantInterface) {
592
                $importantRules[] = $rule->makeImportant();
593
            } else {
594
                $importantRules[] = $rule;
595
            }
596
        }
597
598
        return new self($this->selectors, $importantRules, $this->strictImports);
599
    }
600
601
    /**
602
     * Match arguments.
603
     *
604
     * @param array $args
605
     * @param Context $context
606
     *
607
     * @return bool
608
     */
609
    public function matchArgs(array $args, Context $context)
610
    {
611
        return !is_array($args) || count($args) === 0;
612
    }
613
614
    /**
615
     * Match condition.
616
     *
617
     * @param array $arguments
618
     * @param Context $context
619
     *
620
     * @return bool
621
     */
622
    public function matchCondition(array $arguments, Context $context)
623
    {
624
        $lastSelector = $this->selectors[count($this->selectors) - 1];
625
        if (!$lastSelector->compiledCondition) {
626
            return false;
627
        }
628
629
        if ($lastSelector->condition &&
630
            !$lastSelector->condition->compile(Context::createCopyForCompilation($context, $context->frames))
631
        ) {
632
            return false;
633
        }
634
635
        return true;
636
    }
637
638
    /**
639
     * Returns an array of rulesets.
640
     *
641
     * @return array
642
     */
643
    public function rulesets()
644
    {
645
        $result = [];
646
        foreach ($this->rules as $rule) {
647
            if ($rule instanceof self || $rule instanceof MixinDefinitionNode) {
648
                $result[] = $rule;
649
            }
650
        }
651
652
        return $result;
653
    }
654
655
    /**
656
     * Finds a selector.
657
     *
658
     * @param SelectorNode $selector
659
     * @param RulesetNode $self
660
     * @param Context $context
661
     * @param mixed $filter
662
     *
663
     * @return array
664
     */
665
    public function find(SelectorNode $selector, Context $context, RulesetNode $self = null, $filter = null)
666
    {
667
        $key = $selector->toCSS($context);
668
        if (!$self) {
669
            $self = $this;
670
        }
671
672
        if (!array_key_exists($key, $this->lookups)) {
673
            $rules = [];
674
675
            foreach ($this->rulesets() as $rule) {
676
                if ($rule === $self) {
677
                    continue;
678
                }
679
680
                foreach ($rule->selectors as $ruleSelector) {
681
                    /* @var $ruleSelector SelectorNode */
682
                    $match = $selector->match($ruleSelector);
683
                    if ($match) {
684
                        if (count($selector->elements) > $match) {
685
                            if (!$filter || call_user_func($filter, $rule)) {
686
                                /* @var $rule RulesetNode */
687
                                $foundMixins = $rule->find(new SelectorNode(array_slice($selector->elements, $match)),
688
                                    $context, $self, $filter);
689
                                for ($i = 0; $i < count($foundMixins); ++$i) {
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...
690
                                    array_push($foundMixins[$i]['path'], $rule);
691
                                }
692
                                $rules = array_merge($rules, $foundMixins);
693
                            }
694
                        } else {
695
                            $rules[] = [
696
                                'rule' => $rule,
697
                                'path' => [],
698
                            ];
699
                        }
700
                        break;
701
                    }
702
                }
703
            }
704
705
            $this->lookups[$key] = $rules;
706
        }
707
708
        return $this->lookups[$key];
709
    }
710
711
    /**
712
     * Joins selectors.
713
     *
714
     * @param array $context
715
     * @param array $selectors
716
     *
717
     * @return array
718
     */
719
    public function joinSelectors(array $context, array $selectors)
720
    {
721
        $paths = [];
722
723
        foreach ($selectors as $selector) {
724
            $this->joinSelector($paths, $context, $selector);
725
        }
726
727
        return $paths;
728
    }
729
730
    /**
731
     * Replace all parent selectors inside `$inSelector` by content of `$context` array
732
     * resulting selectors are returned inside `$paths` array
733
     * returns true if `$inSelector` contained at least one parent selector.
734
     *
735
     * @param array $paths
736
     * @param array $context
737
     * @param $inSelector
738
     *
739
     * @return bool
740
     */
741
    private function replaceParentSelector(array &$paths, array $context, $inSelector)
742
    {
743
        $hadParentSelector = false;
744
        $currentElements = [];
745
        $newSelectors = [[]];
746
747
        for ($i = 0; $i < count($inSelector->elements); ++$i) {
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...
748
            $el = $inSelector->elements[$i];
749
            if ($el->value !== '&') {
750
                $nestedSelector = $this->findNestedSelector($el);
751
                if ($nestedSelector !== null) {
752
                    $this->mergeElementsOnToSelectors($currentElements, $newSelectors);
753
                    $nestedPaths = $replacedNewSelectors = [];
754
                    $replaced = $this->replaceParentSelector($nestedPaths, $context, $nestedSelector);
755
                    $hadParentSelector = $hadParentSelector || $replaced;
756
                    // the nestedPaths array should have only one member - replaceParentSelector does not multiply selectors
757
                    for ($k = 0; $k < count($nestedPaths); ++$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...
758
                        $replacementSelector = $this->createSelector($this->createParenthesis($nestedPaths[$k], $el),
759
                            $el);
760
                        $this->addAllReplacementsIntoPath($newSelectors, [$replacementSelector], $el, $inSelector,
761
                            $replacedNewSelectors);
762
                    }
763
                    $newSelectors = $replacedNewSelectors;
764
                    $currentElements = [];
765
                } else {
766
                    $currentElements[] = $el;
767
                }
768
            } else {
769
                $hadParentSelector = true;
770
                // the new list of selectors to add
771
                $selectorsMultiplied = [];
772
                // merge the current list of non parent selector elements
773
                // on to the current list of selectors to add
774
                $this->mergeElementsOnToSelectors($currentElements, $newSelectors);
775
776
                for ($j = 0; $j < count($newSelectors); ++$j) {
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...
777
                    $sel = $newSelectors[$j];
778
                    // if we don't have any parent paths, the & might be in a mixin so that it can be used
779
                    // whether there are parents or not
780
                    if (count($context) === 0) {
781
                        // the combinator used on el should now be applied to the next element instead so that
782
                        // it is not lost
783
                        if (count($sel) > 0) {
784
                            $sel[0]->elements[] = new ElementNode($el->combinator, '', $el->index,
785
                                $el->currentFileInfo);
786
                        }
787
                        $selectorsMultiplied[] = $sel;
788
                    } else {
789
                        // and the parent selectors
790
                        for ($k = 0; $k < count($context); ++$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...
791
                            // We need to put the current selectors
792
                            // then join the last selector's elements on to the parents selectors
793
                            $newSelectorPath = $this->addReplacementIntoPath($sel, $context[$k], $el, $inSelector);
794
                            $selectorsMultiplied[] = $newSelectorPath;
795
                        }
796
                    }
797
                }
798
799
                // our new selectors has been multiplied, so reset the state
800
                $newSelectors = $selectorsMultiplied;
801
802
                $currentElements = [];
803
            }
804
        }
805
806
        // if we have any elements left over (e.g. .a& .b == .b)
807
        // add them on to all the current selectors
808
        $this->mergeElementsOnToSelectors($currentElements, $newSelectors);
809
810
        for ($i = 0; $i < count($newSelectors); ++$i) {
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...
811
            $count = count($newSelectors[$i]);
812
            if ($count > 0) {
813
                $paths[] = &$newSelectors[$i]; // reference the selector!
814
                $lastSelector = $newSelectors[$i][$count - 1];
815
                /* @var $lastSelector SelectorNode */
816
                $newSelectors[$i][$count - 1] = $lastSelector->createDerived($lastSelector->elements,
817
                    $inSelector->extendList);
818
            }
819
        }
820
821
        return $hadParentSelector;
822
    }
823
824
    private function findNestedSelector($element)
825
    {
826
        if (!($element->value instanceof ParenNode)) {
827
            return;
828
        }
829
830
        /* @var $element ParenNode */
831
        $mayBeSelector = $element->value->value;
832
        if (!($mayBeSelector instanceof SelectorNode)) {
833
            return;
834
        }
835
836
        return $mayBeSelector;
837
    }
838
839
    /**
840
     * @param $containedElement
841
     * @param $originalElement
842
     *
843
     * @return SelectorNode
844
     */
845
    private function createSelector($containedElement, $originalElement)
846
    {
847
        $element = new ElementNode(null, $containedElement, $originalElement->index, $originalElement->currentFileInfo);
848
        $selector = new SelectorNode([$element]);
849
850
        return $selector;
851
    }
852
853
    /**
854
     * @param $elementsToPak
855
     * @param $originalElement
856
     *
857
     * @return ParenNode
858
     */
859
    private function createParenthesis($elementsToPak, $originalElement)
860
    {
861
        if (count($elementsToPak) === 0) {
862
            $replacementParen = new ParenNode($elementsToPak[0]);
863
        } else {
864
            $insideParent = [];
865
            for ($j = 0; $j < count($elementsToPak); ++$j) {
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...
866
                $insideParent[] = new ElementNode(null, $elementsToPak[$j], $originalElement->index,
867
                    $originalElement->currentFileInfo);
868
            }
869
            $replacementParen = new ParenNode(new SelectorNode($insideParent));
870
        }
871
872
        return $replacementParen;
873
    }
874
875
    /**
876
     * @param $beginningPath
877
     * @param $addPath
878
     * @param ElementNode $replacedElement
879
     * @param SelectorNode $originalSelector
880
     *
881
     * @return array
882
     */
883
    private function addReplacementIntoPath(
884
        $beginningPath,
885
        $addPath,
886
        ElementNode $replacedElement,
887
        SelectorNode $originalSelector
888
    ) {
889
        // our new selector path
890
        $newSelectorPath = [];
891
892
        // construct the joined selector - if & is the first thing this will be empty,
893
        // if not newJoinedSelector will be the last set of elements in the selector
894
        if (count($beginningPath) > 0) {
895
            $newSelectorPath = $beginningPath;
896
            $lastSelector = array_pop($newSelectorPath);
897
            $newJoinedSelector = $originalSelector->createDerived($lastSelector->elements);
898
        } else {
899
            $newJoinedSelector = $originalSelector->createDerived([]);
900
        }
901
902
        if (count($addPath) > 0) {
903
            $combinator = $replacedElement->combinator;
904
            $parentEl = $addPath[0]->elements[0];
905
            /* @var $parentEl ElementNode */
906
            if ($combinator->emptyOrWhitespace && !$parentEl->combinator->emptyOrWhitespace) {
907
                $combinator = $parentEl->combinator;
908
            }
909
            $newJoinedSelector->elements[] = new ElementNode($combinator, $parentEl->value, $replacedElement->index,
910
                $replacedElement->currentFileInfo);
911
            $newJoinedSelector->elements = array_merge($newJoinedSelector->elements,
912
                array_slice($addPath[0]->elements, 1));
913
        }
914
915
        // now add the joined selector - but only if it is not empty
916
        if (count($newJoinedSelector->elements) !== 0) {
917
            $newSelectorPath[] = $newJoinedSelector;
918
        }
919
920
        if (count($addPath) > 1) {
921
            $newSelectorPath = array_merge($newSelectorPath, array_slice($addPath, 1));
922
        }
923
924
        return $newSelectorPath;
925
    }
926
927
    private function addAllReplacementsIntoPath(
928
        $beginningPath,
929
        $addPaths,
930
        $replacedElement,
931
        $originalSelector,
932
        &$result
933
    ) {
934
        for ($j = 0; $j < count($beginningPath); ++$j) {
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...
935
            $newSelectorPath = $this->addReplacementIntoPath($beginningPath[$j], $addPaths, $replacedElement,
936
                $originalSelector);
937
            $result[] = $newSelectorPath;
938
        }
939
940
        return $result;
941
    }
942
943
    /**
944
     * Joins a selector.
945
     *
946
     * @param array $paths
947
     * @param array $context
948
     * @param SelectorNode $selector The selector
949
     */
950
    private function joinSelector(array &$paths, array $context, SelectorNode $selector)
951
    {
952
        $newPaths = [];
953
        $hasParentSelector = $this->replaceParentSelector($newPaths, $context, $selector);
954
955
        if (!$hasParentSelector) {
956
            if (count($context) > 0) {
957
                $newPaths = [];
958
                for ($i = 0; $i < count($context); ++$i) {
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...
959
                    $newPaths[] = array_merge($context[$i], [$selector]);
960
                }
961
            } else {
962
                $newPaths = [[$selector]];
963
            }
964
        }
965
966
        for ($i = 0; $i < count($newPaths); ++$i) {
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...
967
            $paths[] = $newPaths[$i];
968
        }
969
    }
970
971
    public function mergeElementsOnToSelectors(array $elements, array &$selectors)
972
    {
973
        if (count($elements) === 0) {
974
            return;
975
        }
976
977
        if (count($selectors) === 0) {
978
            $selectors[] = [new SelectorNode($elements)];
979
980
            return;
981
        }
982
983
        foreach ($selectors as &$sel) {
984
            // if the previous thing in sel is a parent this needs to join on to it
985
            if (count($sel) > 0) {
986
                $last = count($sel) - 1;
987
                $sel[$last] = $sel[$last]->createDerived(array_merge($sel[$last]->elements, $elements));
988
            } else {
989
                $sel[] = new SelectorNode($elements);
990
            }
991
        }
992
    }
993
994
    /**
995
     * @return bool
996
     */
997
    public function isRulesetLike()
998
    {
999
        return true;
1000
    }
1001
1002
    public function serialize()
1003
    {
1004
        $vars = get_object_vars($this);
1005
1006
        return Serializer::serialize($vars);
1007
    }
1008
1009
    public function unserialize($serialized)
1010
    {
1011
        $unserialized = Serializer::unserialize($serialized);
1012
        foreach ($unserialized as $var => $val) {
1013
            $this->$var = $val;
1014
        }
1015
    }
1016
}
1017