Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.83% covered (success)
95.83%
368 / 384
73.33% covered (warning)
73.33%
22 / 30
CRAP
0.00% covered (danger)
0.00%
0 / 1
Less_Tree_Ruleset
95.83% covered (success)
95.83%
368 / 384
73.33% covered (warning)
73.33%
22 / 30
193
0.00% covered (danger)
0.00%
0 / 1
 SetRulesetIndex
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 accept
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 compile
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
17
 EvalMixinCalls
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
11
 PrepareRuleset
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
9
 evalImports
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 makeImportant
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 matchArgs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 matchCondition
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 resetCache
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 variables
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 properties
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
10
 variable
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 property
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 transformDeclaration
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
5.01
 lastDeclaration
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 parseValue
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 find
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
13
 isRulesetLikeNode
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 genCSS
98.18% covered (success)
98.18%
54 / 55
0.00% covered (danger)
0.00%
0 / 1
24
 markReferenced
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 getIsReferenced
50.00% covered (danger)
50.00%
5 / 10
0.00% covered (danger)
0.00%
0 / 1
22.50
 joinSelectors
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 joinSelector
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 replaceParentSelector
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
13
 createSelector
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 findNestedSelector
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 addReplacementIntoPath
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
7
 mergeElementsOnToSelectors
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
5.20
1<?php
2declare( strict_types = 1 );
3
4/**
5 * @private
6 */
7class Less_Tree_Ruleset extends Less_Tree {
8
9    /** @var array[][] */
10    protected $lookups;
11    /** @var array<string,Less_Tree_Declaration>|null */
12    public $_variables;
13    /** @var array<string,Less_Tree_Declaration[]>|null */
14    public $_properties;
15
16    /** @var null|bool */
17    public $strictImports;
18
19    /** @var Less_Tree_Selector[]|null */
20    public $selectors;
21    /** @var Less_Tree[] */
22    public $rules;
23    /** @var true|null */
24    public $root;
25    /** @var true|null */
26    public $allowImports;
27    /** @var Less_Tree_Selector[][]|null */
28    public $paths;
29    /** @var true|null */
30    public $firstRoot;
31    /** @var true|null */
32    public $multiMedia;
33    /** @var Less_Tree_Extend[] */
34    public $allExtends;
35
36    /** @var int */
37    public $ruleset_id;
38    /** @var int */
39    public $originalRuleset;
40
41    /** @var array<string,true> */
42    public $first_oelements;
43
44    public function SetRulesetIndex() {
45        $this->ruleset_id = Less_Parser::$next_id++;
46        $this->originalRuleset = $this->ruleset_id;
47
48        if ( $this->selectors ) {
49            foreach ( $this->selectors as $sel ) {
50                if ( $sel->_oelements ) {
51                    $this->first_oelements[$sel->_oelements[0]] = true;
52                }
53            }
54        }
55    }
56
57    /**
58     * @param null|Less_Tree_Selector[] $selectors
59     * @param Less_Tree[] $rules
60     * @param null|bool $strictImports
61     */
62    public function __construct( $selectors, $rules, $strictImports = null ) {
63        $this->selectors = $selectors;
64        $this->rules = $rules;
65        $this->lookups = [];
66        $this->strictImports = $strictImports;
67        $this->SetRulesetIndex();
68    }
69
70    public function accept( $visitor ) {
71        if ( $this->paths !== null ) {
72            $paths_len = count( $this->paths );
73            for ( $i = 0; $i < $paths_len; $i++ ) {
74                $this->paths[$i] = $visitor->visitArray( $this->paths[$i] );
75            }
76        } elseif ( $this->selectors ) {
77            $this->selectors = $visitor->visitArray( $this->selectors );
78        }
79
80        if ( $this->rules ) {
81            $this->rules = $visitor->visitArray( $this->rules );
82        }
83    }
84
85    /**
86     * @param Less_Environment $env
87     * @return self
88     * @see less-2.5.3.js#Ruleset.prototype.eval
89     */
90    public function compile( $env ) {
91        $ruleset = $this->PrepareRuleset( $env );
92
93        // Store the frames around mixin definitions,
94        // so they can be evaluated like closures when the time comes.
95        $rsRuleCnt = count( $ruleset->rules );
96        for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
97            // These checks are the equivalent of the rule.evalFirst property in less.js
98            if ( $ruleset->rules[$i] instanceof Less_Tree_Mixin_Definition || $ruleset->rules[$i] instanceof Less_Tree_DetachedRuleset ) {
99                $ruleset->rules[$i] = $ruleset->rules[$i]->compile( $env );
100            }
101        }
102
103        $mediaBlockCount = count( $env->mediaBlocks );
104
105        // Evaluate mixin calls.
106        $this->EvalMixinCalls( $ruleset, $env, $rsRuleCnt );
107
108        // Evaluate everything else
109        for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
110            if ( !( $ruleset->rules[$i] instanceof Less_Tree_Mixin_Definition || $ruleset->rules[$i] instanceof Less_Tree_DetachedRuleset ) ) {
111                $ruleset->rules[$i] = $ruleset->rules[$i]->compile( $env );
112            }
113        }
114
115        // Evaluate everything else
116        for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
117            $rule = $ruleset->rules[$i];
118
119            // for rulesets, check if it is a css guard and can be removed
120            if ( $rule instanceof self && $rule->selectors && count( $rule->selectors ) === 1 ) {
121
122                // check if it can be folded in (e.g. & where)
123                if ( $rule->selectors[0]->isJustParentSelector() ) {
124                    array_splice( $ruleset->rules, $i--, 1 );
125                    $rsRuleCnt--;
126
127                    for ( $j = 0; $j < count( $rule->rules ); $j++ ) {
128                        $subRule = $rule->rules[$j];
129                        if ( !( $subRule instanceof Less_Tree_Declaration ) || !$subRule->variable ) {
130                            array_splice( $ruleset->rules, ++$i, 0, [ $subRule ] );
131                            $rsRuleCnt++;
132                        }
133                    }
134
135                }
136            }
137        }
138
139        // Pop the stack
140        $env->shiftFrame();
141
142        if ( $mediaBlockCount ) {
143            $len = count( $env->mediaBlocks );
144            for ( $i = $mediaBlockCount; $i < $len; $i++ ) {
145                $env->mediaBlocks[$i]->bubbleSelectors( $ruleset->selectors );
146            }
147        }
148
149        return $ruleset;
150    }
151
152    /**
153     * Compile Less_Tree_Mixin_Call objects
154     *
155     * @param self $ruleset
156     * @param Less_Environment $env
157     * @param int &$rsRuleCnt
158     */
159    private function EvalMixinCalls( $ruleset, $env, &$rsRuleCnt ) {
160        for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
161            $rule = $ruleset->rules[$i];
162
163            if ( $rule instanceof Less_Tree_Mixin_Call ) {
164                $rule = $rule->compile( $env );
165
166                $temp = [];
167                foreach ( $rule as $r ) {
168                    if ( ( $r instanceof Less_Tree_Declaration ) && $r->variable ) {
169                        // do not pollute the scope if the variable is
170                        // already there. consider returning false here
171                        // but we need a way to "return" variable from mixins
172                        if ( !$ruleset->variable( $r->name ) ) {
173                            $temp[] = $r;
174                        }
175                    } else {
176                        $temp[] = $r;
177                    }
178                }
179                $temp_count = count( $temp ) - 1;
180                array_splice( $ruleset->rules, $i, 1, $temp );
181                $rsRuleCnt += $temp_count;
182                $i += $temp_count;
183                $ruleset->resetCache();
184
185            } elseif ( $rule instanceof Less_Tree_VariableCall ) {
186
187                $rule = $rule->compile( $env );
188                $rules = [];
189                foreach ( $rule->rules as $r ) {
190                    if ( ( $r instanceof Less_Tree_Declaration ) && $r->variable ) {
191                        continue;
192                    }
193                    $rules[] = $r;
194                }
195
196                array_splice( $ruleset->rules, $i, 1, $rules );
197                $temp_count = count( $rules );
198                $rsRuleCnt += $temp_count - 1;
199                $i += $temp_count - 1;
200                $ruleset->resetCache();
201            }
202
203        }
204    }
205
206    /**
207     * Compile the selectors and create a new ruleset object for the compile() method
208     *
209     * @param Less_Environment $env
210     * @return self
211     */
212    private function PrepareRuleset( $env ) {
213        // NOTE: Preserve distinction between null and empty array when compiling
214        // $this->selectors to $selectors
215        $thisSelectors = $this->selectors;
216        $selectors = null;
217        $hasOnePassingSelector = false;
218
219        if ( $thisSelectors ) {
220            Less_Tree_DefaultFunc::error( "it is currently only allowed in parametric mixin guards," );
221
222            $selectors = [];
223            foreach ( $thisSelectors as $s ) {
224                $selector = $s->compile( $env );
225                $selectors[] = $selector;
226                if ( $selector->evaldCondition ) {
227                    $hasOnePassingSelector = true;
228                }
229            }
230
231            Less_Tree_DefaultFunc::reset();
232        } else {
233            $hasOnePassingSelector = true;
234        }
235
236        if ( $this->rules && $hasOnePassingSelector ) {
237            // Copy the array (no need for slice in PHP)
238            $rules = $this->rules;
239        } else {
240            $rules = [];
241        }
242
243        $ruleset = new self( $selectors, $rules, $this->strictImports );
244
245        $ruleset->originalRuleset = $this->ruleset_id;
246        $ruleset->root = $this->root;
247        $ruleset->firstRoot = $this->firstRoot;
248        $ruleset->allowImports = $this->allowImports;
249
250        // push the current ruleset to the frames stack
251        $env->unshiftFrame( $ruleset );
252
253        // Evaluate imports
254        if ( $ruleset->root || $ruleset->allowImports || !$ruleset->strictImports ) {
255            $ruleset->evalImports( $env );
256        }
257
258        return $ruleset;
259    }
260
261    public function evalImports( $env ) {
262        $rules_len = count( $this->rules );
263        for ( $i = 0; $i < $rules_len; $i++ ) {
264            $rule = $this->rules[$i];
265
266            if ( $rule instanceof Less_Tree_Import ) {
267                $rules = $rule->compile( $env );
268                if ( is_array( $rules ) ) {
269                    array_splice( $this->rules, $i, 1, $rules );
270                    $temp_count = count( $rules ) - 1;
271                    $i += $temp_count;
272                    $rules_len += $temp_count;
273                } else {
274                    array_splice( $this->rules, $i, 1, [ $rules ] );
275                }
276
277                $this->resetCache();
278            }
279        }
280    }
281
282    public function makeImportant() {
283        $important_rules = [];
284        foreach ( $this->rules as $rule ) {
285            if ( $rule instanceof Less_Tree_Declaration || $rule instanceof self || $rule instanceof Less_Tree_NameValue ) {
286                $important_rules[] = $rule->makeImportant();
287            } else {
288                $important_rules[] = $rule;
289            }
290        }
291
292        return new self( $this->selectors, $important_rules, $this->strictImports );
293    }
294
295    public function matchArgs( $args, $env = null ) {
296        return !$args;
297    }
298
299    // lets you call a css selector with a guard
300    public function matchCondition( $args, $env ) {
301        $lastSelector = end( $this->selectors );
302
303        if ( !$lastSelector->evaldCondition ) {
304            return false;
305        }
306        if ( $lastSelector->condition && !$lastSelector->condition->compile( $env->copyEvalEnv( $env->frames ) ) ) {
307            return false;
308        }
309        return true;
310    }
311
312    public function resetCache() {
313        $this->_variables = null;
314        $this->lookups = [];
315    }
316
317    /**
318     * @see less-3.13.1.js#Ruleset.prototype.variables
319     */
320    public function variables() {
321        $this->_variables = [];
322        foreach ( $this->rules as $r ) {
323            if ( $r instanceof Less_Tree_Declaration && $r->variable === true ) {
324                $this->_variables[$r->name] = $r;
325            }
326            // when evaluating variables in an import statement, imports have not been eval'd
327            // so we need to go inside import statements.
328            // guard against root being a string (in the case of inlined less)
329            if ( $r instanceof Less_Tree_Import && $r->root instanceof Less_Tree_Ruleset ) {
330                $vars = $r->root->variables();
331                foreach ( $vars as $key => $name ) {
332                    $this->_variables[$key] = $name;
333                }
334            }
335        }
336        return $this->_variables;
337    }
338
339    /**
340     * @see less-3.13.1#Ruleset.prototype.properties
341     */
342    public function properties() {
343        $this->_properties = [];
344        foreach ( $this->rules as $r ) {
345
346            if ( $r instanceof Less_Tree_Declaration && $r->variable !== true ) {
347                $name = is_array( $r->name ) && count( $r->name ) === 1 && $r->name[0] instanceof Less_Tree_Keyword
348                    ? $r->name[0]->value
349                    : $r->name;
350                // Properties don't overwrite as they can merge
351
352                // TODO: differs from upstream. Upstream expects $r->name to be only a
353                // Less_Tree_Keyword but somehow our parser also returns Less_Tree_Property.
354                // Let's handle it for now, but we should debug why this happens
355                // caused by test/Fixtures/lessjs-3.13.1/less/_main/property-accessors.less:59
356                if ( is_array( $name ) && $name[0] instanceof Less_Tree_Property ) {
357                    $name = $name[0]->name;
358                }
359
360                $idx = '$' . $name;
361                if ( !array_key_exists( $idx, $this->_properties ) ) {
362                    $this->_properties[ $idx ] = [];
363                }
364                $this->_properties[ $idx ][] = $r;
365            }
366        }
367        return $this->_properties;
368    }
369
370    /**
371     * @param string $name
372     * @return Less_Tree_Declaration|null
373     * @see less-3.13.1#Ruleset.prototype.variable
374     */
375    public function variable( $name ) {
376        if ( $this->_variables === null ) {
377            $this->variables();
378        }
379        return array_key_exists( $name, $this->_variables )
380            ? $this->parseValue( $this->_variables[ $name ] )
381            : null;
382    }
383
384    /**
385     * @param string $name
386     * @see less-3.13.1#Ruleset.prototype.property
387     */
388    public function property( $name ) {
389        if ( $this->_properties === null ) {
390            $this->properties();
391        }
392        return array_key_exists( $name, $this->_properties )
393            ? $this->parseValue( $this->_properties[ $name ] )
394            : null;
395    }
396
397    /**
398     * @param Less_Tree_Declaration $decl
399     * @return mixed
400     * @throws Less_Exception_Parser
401     */
402    private function transformDeclaration( $decl ) {
403        if ( $decl->value instanceof Less_Tree_Anonymous && !$decl->parsed ) {
404            [ $err, $result ] = self::$parse->parseNode(
405                (string)$decl->value->value,
406                [ 'value', 'important' ],
407                $decl->value->index,
408                $decl->value->currentFileInfo ?? []
409            );
410            if ( $err ) {
411                $decl->parsed = true;
412            }
413            if ( $result ) {
414                $decl->value = $result[0];
415                $decl->important = $result[1] ?? '';
416                $decl->parsed = true;
417            }
418            return $decl;
419        } else {
420            return $decl;
421        }
422    }
423
424    public function lastDeclaration() {
425        for ( $i = count( $this->rules ); $i > 0; $i-- ) {
426            $decl = $this->rules[ $i - 1 ];
427            if ( $decl instanceof Less_Tree_Declaration ) {
428                return $this->parseValue( $decl );
429            }
430        }
431    }
432
433    private function parseValue( $toParse ) {
434        if ( !is_array( $toParse ) ) {
435            return $this->transformDeclaration( $toParse );
436        } else {
437            $nodes = [];
438            foreach ( $toParse as $n ) {
439                $nodes[] = $this->transformDeclaration( $n );
440            }
441            return $nodes;
442        }
443    }
444
445    public function find( $selector, $self = null, $filter = null ) {
446        $key = implode( ' ', $selector->_oelements );
447
448        if ( !isset( $this->lookups[$key] ) ) {
449
450            if ( !$self ) {
451                $self = $this->ruleset_id;
452            }
453
454            $this->lookups[$key] = [];
455
456            $first_oelement = $selector->_oelements[0];
457
458            foreach ( $this->rules as $rule ) {
459                if ( $rule instanceof self && $rule->ruleset_id != $self ) {
460
461                    if ( isset( $rule->first_oelements[$first_oelement] ) ) {
462
463                        foreach ( $rule->selectors as $ruleSelector ) {
464                            $match = $selector->match( $ruleSelector );
465                            if ( $match ) {
466                                if ( $selector->elements_len > $match ) {
467                                    if ( !$filter || $filter( $rule ) ) {
468                                        $foundMixins = $rule->find( new Less_Tree_Selector( array_slice( $selector->elements, $match ) ), $self, $filter );
469                                        for ( $i = 0; $i < count( $foundMixins ); ++$i ) {
470                                            $foundMixins[$i]["path"][] = $rule;
471                                        }
472                                        $this->lookups[$key] = array_merge( $this->lookups[$key], $foundMixins );
473                                    }
474                                } else {
475                                    $this->lookups[$key][] = [ "rule" => $rule, "path" => [] ];
476                                }
477                                break;
478                            }
479                        }
480                    }
481                }
482            }
483
484        }
485
486        return $this->lookups[$key];
487    }
488
489    private function isRulesetLikeNode( $rule ) {
490        // if it has nested rules, then it should be treated like a ruleset
491        // medias and comments do not have nested rules, but should be treated like rulesets anyway
492        // some directives and anonymous nodes are ruleset like, others are not
493        if ( $rule instanceof Less_Tree_Media || $rule instanceof Less_Tree_Ruleset ) {
494            return true;
495        } elseif ( $rule instanceof Less_Tree_Anonymous || $rule instanceof Less_Tree_AtRule ) {
496            return $rule->isRulesetLike();
497        }
498
499        // anything else is assumed to be a rule
500        return false;
501    }
502
503    /**
504     * @param Less_Output $output
505     * @see less-2.5.3.js#Ruleset.prototype.genCSS
506     */
507    public function genCSS( $output ) {
508        if ( !$this->root ) {
509            Less_Environment::$tabLevel++;
510        }
511
512        $tabRuleStr = $tabSetStr = '';
513        if ( !Less_Parser::$options['compress'] ) {
514            if ( Less_Environment::$tabLevel ) {
515                $tabRuleStr = "\n" . str_repeat( Less_Parser::$options['indentation'], Less_Environment::$tabLevel );
516                $tabSetStr = "\n" . str_repeat( Less_Parser::$options['indentation'], Less_Environment::$tabLevel - 1 );
517            } else {
518                $tabSetStr = $tabRuleStr = "\n";
519            }
520        }
521
522        $ruleNodes = [];
523        $charsetNodeIndex = 0;
524        $importNodeIndex = 0;
525        foreach ( $this->rules as $i => $rule ) {
526            if ( $rule instanceof Less_Tree_Comment ) {
527                if ( $importNodeIndex === $i ) {
528                    $importNodeIndex++;
529                }
530                $ruleNodes[] = $rule;
531            } elseif ( $rule instanceof Less_Tree_AtRule && $rule->isCharset() ) {
532                array_splice( $ruleNodes, $charsetNodeIndex, 0, [ $rule ] );
533                $charsetNodeIndex++;
534                $importNodeIndex++;
535            } elseif ( $rule instanceof Less_Tree_Import ) {
536                array_splice( $ruleNodes, $importNodeIndex, 0, [ $rule ] );
537                $importNodeIndex++;
538            } else {
539                $ruleNodes[] = $rule;
540            }
541        }
542
543        // If this is the root node, we don't render
544        // a selector, or {}.
545        if ( !$this->root ) {
546
547            $sep = ',' . $tabSetStr;
548            // TODO: Move to Env object
549            // TODO: Inject Env object to toCSS() and genCSS()
550            $firstSelector = false;
551
552            foreach ( $this->paths as $i => $path ) {
553                $pathSubCnt = count( $path );
554                if ( !$pathSubCnt ) {
555                    continue;
556                }
557                if ( $i > 0 ) {
558                    $output->add( $sep );
559                }
560                $firstSelector = true;
561                $path[0]->genCSS( $output, $firstSelector );
562                $firstSelector = false;
563                for ( $j = 1; $j < $pathSubCnt; $j++ ) {
564                    $path[$j]->genCSS( $output, $firstSelector );
565                }
566            }
567
568            $output->add( ( Less_Parser::$options['compress'] ? '{' : " {" ) . $tabRuleStr );
569        }
570
571        // Compile rules and rulesets
572        foreach ( $ruleNodes as $i => $rule ) {
573
574            if ( $i + 1 === count( $ruleNodes ) ) {
575                Less_Environment::$lastRule = true;
576            }
577            $currentLastRule = Less_Environment::$lastRule;
578
579            if ( $this->isRulesetLikeNode( $rule ) ) {
580                Less_Environment::$lastRule = false;
581            }
582
583            $rule->genCSS( $output );
584
585            Less_Environment::$lastRule = $currentLastRule;
586
587            if ( !Less_Environment::$lastRule && $rule->isVisible() ) {
588                $output->add( $tabRuleStr );
589            } else {
590                Less_Environment::$lastRule = false;
591            }
592        }
593
594        if ( !$this->root ) {
595            $output->add( $tabSetStr . '}' );
596            Less_Environment::$tabLevel--;
597        }
598
599        if ( !Less_Parser::$options['compress'] && $this->firstRoot ) {
600            $output->add( "\n" );
601        }
602    }
603
604    public function markReferenced() {
605        if ( $this->selectors !== null ) {
606            foreach ( $this->selectors as $selector ) {
607                $selector->markReferenced();
608            }
609        }
610
611        if ( $this->rules ) {
612            foreach ( $this->rules as $rule ) {
613                if ( method_exists( $rule, 'markReferenced' ) ) {
614                    $rule->markReferenced();
615                }
616            }
617        }
618    }
619
620    public function getIsReferenced() {
621        if ( $this->paths ) {
622            foreach ( $this->paths as $path ) {
623                foreach ( $path as $p ) {
624                    if ( method_exists( $p, 'getIsReferenced' ) && $p->getIsReferenced() ) {
625                        return true;
626                    }
627                }
628            }
629        }
630
631        if ( $this->selectors ) {
632            foreach ( $this->selectors as $selector ) {
633                if ( method_exists( $selector, 'getIsReferenced' ) && $selector->getIsReferenced() ) {
634                    return true;
635                }
636            }
637        }
638
639        return false;
640    }
641
642    /**
643     * @param Less_Tree_Selector[][] $context
644     * @param Less_Tree_Selector[]|null $selectors
645     * @return Less_Tree_Selector[][]
646     */
647    public function joinSelectors( $context, $selectors ) {
648        $paths = [];
649        if ( $selectors !== null ) {
650            foreach ( $selectors as $selector ) {
651                $this->joinSelector( $paths, $context, $selector );
652            }
653        }
654        return $paths;
655    }
656
657    public function joinSelector( array &$paths, array $context, Less_Tree_Selector $selector ) {
658        $newPaths = [];
659        $hadParentSelector = $this->replaceParentSelector( $newPaths, $context, $selector );
660
661        if ( !$hadParentSelector ) {
662            if ( $context ) {
663                $newPaths = [];
664                foreach ( $context as $path ) {
665                    $newPaths[] = array_merge( $path, [ $selector ] );
666                }
667            } else {
668                $newPaths = [ [ $selector ] ];
669            }
670        }
671
672        foreach ( $newPaths as $newPath ) {
673            $paths[] = $newPath;
674        }
675    }
676
677    /**
678     * Replace all parent selectors inside $inSelector with $context.
679     *
680     * @param array &$paths Resulting selectors are appended to $paths.
681     * @param mixed $context
682     * @param Less_Tree_Selector $inSelector Inner selector from Less_Tree_Paren
683     * @return bool True if $inSelector contained at least one parent selector
684     */
685    private function replaceParentSelector( array &$paths, $context, Less_Tree_Selector $inSelector ) {
686        $hadParentSelector = false;
687
688        // The paths are [[Selector]]
689        // The first list is a list of comma separated selectors
690        // The inner list is a list of inheritance separated selectors
691        // e.g.
692        // .a, .b {
693        //   .c {
694        //   }
695        // }
696        // == [[.a] [.c]] [[.b] [.c]]
697        //
698
699        // the elements from the current selector so far
700        $currentElements = [];
701        // the current list of new selectors to add to the path.
702        // We will build it up. We initiate it with one empty selector as we "multiply" the new selectors
703        // by the parents
704        $newSelectors = [
705            []
706        ];
707
708        foreach ( $inSelector->elements as $el ) {
709            // non-parent reference elements just get added
710            if ( $el->value !== '&' ) {
711                $nestedSelector = $this->findNestedSelector( $el );
712                if ( $nestedSelector !== null ) {
713                    $this->mergeElementsOnToSelectors( $currentElements, $newSelectors );
714
715                    $nestedPaths = [];
716                    $replacedNewSelectors = [];
717                    $replaced = $this->replaceParentSelector( $nestedPaths, $context, $nestedSelector );
718                    $hadParentSelector = $hadParentSelector || $replaced;
719                    // $nestedPaths is populated by replaceParentSelector()
720                    // $nestedPaths should have exactly one TODO, replaceParentSelector does not multiply selectors
721                    foreach ( $nestedPaths as $nestedPath ) {
722                        $replacementSelector = $this->createSelector( $nestedPath, $el );
723
724                        // join selector path from $newSelectors with every selector path in $addPaths array.
725                        // $el contains the element that is being replaced by $addPaths
726                        //
727                        // @see less-2.5.3.js#Ruleset-addAllReplacementsIntoPath
728                        $addPaths = [ $replacementSelector ];
729                        foreach ( $newSelectors as $newSelector ) {
730                            $replacedNewSelectors[] = $this->addReplacementIntoPath( $newSelector, $addPaths, $el, $inSelector );
731                        }
732                    }
733                    $newSelectors = $replacedNewSelectors;
734                    $currentElements = [];
735                } else {
736                    $currentElements[] = $el;
737                }
738            } else {
739                $hadParentSelector = true;
740
741                // the new list of selectors to add
742                $selectorsMultiplied = [];
743
744                // merge the current list of non parent selector elements
745                // on to the current list of selectors to add
746                $this->mergeElementsOnToSelectors( $currentElements, $newSelectors );
747
748                foreach ( $newSelectors as $sel ) {
749                    // if we don't have any parent paths, the & might be in a mixin so that it can be used
750                    // whether there are parents or not
751                    if ( !$context ) {
752                        // the combinator used on el should now be applied to the next element instead so that
753                        // it is not lost
754                        if ( $sel ) {
755                            $sel[0]->elements[] = new Less_Tree_Element( $el->combinator, '', $el->index, $el->currentFileInfo );
756                        }
757                        $selectorsMultiplied[] = $sel;
758                    } else {
759                        // and the parent selectors
760                        foreach ( $context as $parentSel ) {
761                            // We need to put the current selectors
762                            // then join the last selector's elements on to the parents selectors
763                            $newSelectorPath = $this->addReplacementIntoPath( $sel, $parentSel, $el, $inSelector );
764                            // add that to our new set of selectors
765                            $selectorsMultiplied[] = $newSelectorPath;
766                        }
767                    }
768                }
769
770                // our new selectors has been multiplied, so reset the state
771                $newSelectors = $selectorsMultiplied;
772                $currentElements = [];
773            }
774        }
775
776        // if we have any elements left over (e.g. .a& .b == .b)
777        // add them on to all the current selectors
778        $this->mergeElementsOnToSelectors( $currentElements, $newSelectors );
779
780        foreach ( $newSelectors as &$sel ) {
781            $length = count( $sel );
782            if ( $length ) {
783                $lastSelector = $sel[$length - 1];
784                $sel[$length - 1] = $lastSelector->createDerived( $lastSelector->elements, $inSelector->extendList );
785                $paths[] = $sel;
786            }
787        }
788
789        return $hadParentSelector;
790    }
791
792    /**
793     * @param array $elementsToPak
794     * @param Less_Tree_Element $originalElement
795     * @return Less_Tree_Selector
796     */
797    private function createSelector( array $elementsToPak, $originalElement ) {
798        if ( !$elementsToPak ) {
799            // This is an invalid call. Kept to match less.js. Appears unreachable.
800            // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal
801            $containedElement = new Less_Tree_Paren( null );
802        } else {
803            $insideParent = [];
804            foreach ( $elementsToPak as $elToPak ) {
805                $insideParent[] = new Less_Tree_Element( null, $elToPak, $originalElement->index, $originalElement->currentFileInfo );
806            }
807            $containedElement = new Less_Tree_Paren( new Less_Tree_Selector( $insideParent ) );
808        }
809
810        $element = new Less_Tree_Element( null, $containedElement, $originalElement->index, $originalElement->currentFileInfo );
811        return new Less_Tree_Selector( [ $element ] );
812    }
813
814    /**
815     * @param Less_Tree_Element $element
816     * @return Less_Tree_Selector|null
817     */
818    private function findNestedSelector( $element ) {
819        $maybeParen = $element->value;
820        if ( !( $maybeParen instanceof Less_Tree_Paren ) ) {
821            return null;
822        }
823        $maybeSelector = $maybeParen->value;
824        if ( !( $maybeSelector instanceof Less_Tree_Selector ) ) {
825            return null;
826        }
827        return $maybeSelector;
828    }
829
830    /**
831     * joins selector path from $beginningPath with selector path in $addPath.
832     *
833     * $replacedElement contains the element that is being replaced by $addPath
834     *
835     * @param Less_Tree_Selector[] $beginningPath
836     * @param Less_Tree_Selector[] $addPath
837     * @param Less_Tree_Element $replacedElement
838     * @param Less_Tree_Selector $originalSelector
839     * @return Less_Tree_Selector[] Concatenated path
840     * @see less-2.5.3.js#Ruleset-addReplacementIntoPath
841     */
842    private function addReplacementIntoPath( array $beginningPath, array $addPath, $replacedElement, $originalSelector ) {
843        // our new selector path
844        $newSelectorPath = [];
845
846        // construct the joined selector - if `&` is the first thing this will be empty,
847        // if not newJoinedSelector will be the last set of elements in the selector
848        if ( $beginningPath ) {
849            // NOTE: less.js uses Array slice() to copy. In PHP, arrays are naturally copied by value.
850            $newSelectorPath = $beginningPath;
851            $lastSelector = array_pop( $newSelectorPath );
852            $newJoinedSelector = $originalSelector->createDerived( $lastSelector->elements );
853        } else {
854            $newJoinedSelector = $originalSelector->createDerived( [] );
855        }
856
857        if ( $addPath ) {
858            // if the & does not have a combinator that is "" or " " then
859            // and there is a combinator on the parent, then grab that.
860            // this also allows `+ a { & .b { .a & { ...`
861            $combinator = $replacedElement->combinator;
862            $parentEl = $addPath[0]->elements[0];
863            if ( $replacedElement->combinatorIsEmptyOrWhitespace && !$parentEl->combinatorIsEmptyOrWhitespace ) {
864                $combinator = $parentEl->combinator;
865            }
866            // join the elements so far with the first part of the parent
867            $newJoinedSelector->elements[] = new Less_Tree_Element( $combinator, $parentEl->value, $replacedElement->index, $replacedElement->currentFileInfo );
868            $newJoinedSelector->elements = array_merge(
869                $newJoinedSelector->elements,
870                array_slice( $addPath[0]->elements, 1 )
871            );
872        }
873
874        // now add the joined selector - but only if it is not empty
875        if ( $newJoinedSelector->elements ) {
876            $newSelectorPath[] = $newJoinedSelector;
877        }
878
879        // put together the parent selectors after the join (e.g. the rest of the parent)
880        if ( count( $addPath ) > 1 ) {
881            $newSelectorPath = array_merge( $newSelectorPath, array_slice( $addPath, 1 ) );
882        }
883        return $newSelectorPath;
884    }
885
886    public function mergeElementsOnToSelectors( $elements, &$selectors ) {
887        if ( !$elements ) {
888            return;
889        }
890        if ( !$selectors ) {
891            $selectors[] = [ new Less_Tree_Selector( $elements ) ];
892            return;
893        }
894
895        foreach ( $selectors as &$sel ) {
896            // if the previous thing in sel is a parent this needs to join on to it
897            if ( $sel ) {
898                $last = count( $sel ) - 1;
899                $sel[$last] = $sel[$last]->createDerived( array_merge( $sel[$last]->elements, $elements ) );
900            } else {
901                $sel[] = new Less_Tree_Selector( $elements );
902            }
903        }
904    }
905}