Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.77% covered (warning)
74.77%
83 / 111
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Less_Tree_Mixin_Call
74.77% covered (warning)
74.77%
83 / 111
25.00% covered (danger)
25.00%
1 / 4
87.54
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 compile
81.82% covered (warning)
81.82%
72 / 88
0.00% covered (danger)
0.00%
0 / 1
45.23
 Format
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 IsRecursive
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
6.10
1<?php
2declare( strict_types = 1 );
3
4/**
5 * @private
6 */
7class Less_Tree_Mixin_Call extends Less_Tree {
8
9    /** @var Less_Tree_Selector */
10    public $selector;
11    /** @var array[] */
12    public $arguments;
13    /** @var int */
14    public $index;
15    /** @var array */
16    public $currentFileInfo;
17
18    /** @var bool */
19    public $important;
20
21    public function __construct( $elements, $args, $index, $currentFileInfo, $important = false ) {
22        $this->selector = new Less_Tree_Selector( $elements );
23        $this->arguments = $args;
24        $this->index = $index;
25        $this->currentFileInfo = $currentFileInfo;
26        $this->important = $important;
27    }
28
29    /**
30     * @see less-2.5.3.js#MixinCall.prototype.eval
31     */
32    public function compile( $env ) {
33        $rules = [];
34        $match = false;
35        $isOneFound = false;
36        $candidates = [];
37        $conditionResult = [];
38        $this->selector = $this->selector->compile( $env );
39        $args = [];
40        foreach ( $this->arguments as $a ) {
41            $argValue = $a['value']->compile( $env );
42            if ( !empty( $a['expand'] ) && is_array( $argValue->value ) ) {
43                foreach ( $argValue->value as $value ) {
44                    $args[] = [ 'name' => null, 'value' => $value ];
45                }
46            } else {
47                $args[] = [ 'name' => $a['name'], 'value' => $argValue ];
48            }
49        }
50
51        $defNone = 0;
52        $defTrue = 1;
53        $defFalse = 2;
54        foreach ( $env->frames as $frame ) {
55            $noArgumentsFilter = static function ( $rule ) use ( $env ) {
56                return $rule->matchArgs( [], $env );
57            };
58            $mixins = $frame->find( $this->selector, null, $noArgumentsFilter );
59            if ( !$mixins ) {
60                continue;
61            }
62
63            $isOneFound = true;
64
65            // To make `default()` function independent of definition order we have two "subpasses" here.
66            // At first we evaluate each guard *twice* (with `default() == true` and `default() == false`),
67            // and build candidate list with corresponding flags. Then, when we know all possible matches,
68            // we make a final decision.
69
70            $mixins_len = count( $mixins );
71            for ( $m = 0; $m < $mixins_len; $m++ ) {
72                $mixin = $mixins[$m]["rule"];
73                $mixinPath = $mixins[$m]["path"];
74
75                if ( $this->IsRecursive( $env, $mixin ) ) {
76                    continue;
77                }
78
79                if ( $mixin->matchArgs( $args, $env ) ) {
80
81                    // less-2.5.3.js#MixinCall calcDefGroup()
82                    $group = -1;
83                    for ( $f = 0; $f < 2; $f++ ) {
84                        $conditionResult[$f] = true;
85                        Less_Tree_DefaultFunc::value( $f );
86                        for ( $p = 0; $p < count( $mixinPath ) && $conditionResult[$f]; $p++ ) {
87                            $namespace = $mixinPath[$p] ?? null;
88                            if ( isset( $namespace ) && method_exists( $namespace, "matchCondition" ) ) {
89                                $conditionResult[$f] = $conditionResult[$f] && $namespace->matchCondition( null, $env );
90                            }
91                        }
92                        if ( method_exists( $mixin, "matchCondition" ) ) {
93                            $conditionResult[$f] = $conditionResult[$f] && $mixin->matchCondition( $args, $env );
94                        }
95                    }
96                    // PhanTypeInvalidDimOffset -- False positive
97                    '@phan-var array{0:bool,1:bool} $conditionResult';
98                    if ( $conditionResult[0] || $conditionResult[1] ) {
99                        if ( $conditionResult[0] != $conditionResult[1] ) {
100                            $group = $conditionResult[1] ?
101                                $defTrue : $defFalse;
102                        } else {
103                            $group = $defNone;
104                        }
105
106                    }
107
108                    $candidate = [ 'mixin' => $mixin, 'group' => $group ];
109
110                    if ( $candidate["group"] !== -1 ) {
111                        $candidates[] = $candidate;
112                    }
113
114                    $match = true;
115                }
116            }
117
118            Less_Tree_DefaultFunc::reset();
119
120            $count = [ 0, 0, 0 ];
121            for ( $m = 0; $m < count( $candidates ); $m++ ) {
122                $count[ $candidates[$m]['group'] ]++;
123            }
124
125            if ( $count[$defNone] > 0 ) {
126                $defaultResult = $defFalse;
127            } else {
128                $defaultResult = $defTrue;
129                if ( ( $count[$defTrue] + $count[$defFalse] ) > 1 ) {
130                    throw new Exception( 'Ambiguous use of `default()` found when matching for `' . $this->format( $args ) . '`' );
131                }
132            }
133
134            $candidates_length = count( $candidates );
135            for ( $m = 0; $m < $candidates_length; $m++ ) {
136                $candidate = $candidates[$m]['group'];
137                if ( ( $candidate === $defNone ) || ( $candidate === $defaultResult ) ) {
138                    try {
139                        $mixin = $candidates[$m]['mixin'];
140                        if ( !( $mixin instanceof Less_Tree_Mixin_Definition ) ) {
141                            $originalRuleset = $mixin instanceof Less_Tree_Ruleset ? $mixin->originalRuleset : $mixin;
142                            $mixin = new Less_Tree_Mixin_Definition( '', [], $mixin->rules, null, false );
143                            $mixin->originalRuleset = $originalRuleset;
144                        }
145                        $rules = array_merge( $rules, $mixin->evalCall( $env, $args, $this->important )->rules );
146                    } catch ( Exception $e ) {
147                        throw new Less_Exception_Compiler( $e->getMessage(), null, null, $this->currentFileInfo );
148                    }
149                }
150            }
151
152            if ( $match ) {
153                if ( !$this->currentFileInfo || !isset( $this->currentFileInfo['reference'] ) || !$this->currentFileInfo['reference'] ) {
154                    Less_Tree::ReferencedArray( $rules );
155                }
156
157                return $rules;
158            }
159        }
160
161        if ( $isOneFound ) {
162            $selectorName = $this->selector->toCSS();
163            throw new Less_Exception_Compiler(
164                'No matching definition was found for ' . $selectorName . ' with args `' . $this->Format( $args ) . '`',
165                null,
166                $this->index,
167                $this->currentFileInfo
168            );
169
170        } else {
171            throw new Less_Exception_Compiler(
172                trim( $this->selector->toCSS() ) . " is undefined in " . $this->currentFileInfo['filename'],
173                null,
174                $this->index
175            );
176        }
177    }
178
179    /**
180     * Format the args for use in exception messages
181     */
182    private function Format( $args ) {
183        $message = [];
184        if ( $args ) {
185            foreach ( $args as $a ) {
186                $argValue = '';
187                if ( $a['name'] ) {
188                    $argValue .= $a['name'] . ':';
189                }
190                if ( $a['value'] instanceof Less_Tree ) {
191                    $argValue .= $a['value']->toCSS();
192                } else {
193                    $argValue .= '???';
194                }
195                $message[] = $argValue;
196            }
197        }
198        return implode( ', ', $message );
199    }
200
201    /**
202     * Are we in a recursive mixin call?
203     *
204     * @return bool
205     */
206    private function IsRecursive( $env, $mixin ) {
207        foreach ( $env->frames as $recur_frame ) {
208            if ( !( $mixin instanceof Less_Tree_Mixin_Definition ) ) {
209
210                if ( $mixin === $recur_frame ) {
211                    return true;
212                }
213
214                if ( isset( $recur_frame->originalRuleset ) && $mixin->ruleset_id === $recur_frame->originalRuleset ) {
215                    return true;
216                }
217            }
218        }
219
220        return false;
221    }
222
223}