MixinDefinitionNode::compile()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 8
nc 1
nop 3
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\Exception\CompilerException;
14
use ILess\Visitor\VisitorInterface;
15
16
/**
17
 * MixinDefinition.
18
 */
19
class MixinDefinitionNode extends RulesetNode
20
{
21
    /**
22
     * Node type.
23
     *
24
     * @var string
25
     */
26
    protected $type = 'MixinDefinition';
27
28
    /**
29
     * The definition name.
30
     *
31
     * @var string
32
     */
33
    public $name;
34
35
    /**
36
     * Array of selectors.
37
     *
38
     * @var array
39
     */
40
    public $selectors;
41
42
    /**
43
     * Array of parameters.
44
     *
45
     * @var array
46
     */
47
    public $params = [];
48
49
    /**
50
     * The arity.
51
     *
52
     * @var int
53
     */
54
    public $arity = 0;
55
56
    /**
57
     * Array of rules.
58
     *
59
     * @var array
60
     */
61
    public $rules = [];
62
63
    /**
64
     * Lookups cache array.
65
     *
66
     * @var array
67
     */
68
    public $lookups = [];
69
70
    /**
71
     * Number of required parameters.
72
     *
73
     * @var int
74
     */
75
    public $required = 0;
76
77
    /**
78
     * Frames array.
79
     *
80
     * @var array
81
     */
82
    public $frames = [];
83
84
    /**
85
     * The condition.
86
     *
87
     * @var ConditionNode
88
     */
89
    public $condition;
90
91
    /**
92
     * Variadic flag.
93
     *
94
     * @var bool
95
     */
96
    public $variadic = false;
97
98
    /**
99
     * @var bool
100
     */
101
    public $compileFirst = true;
102
103
    /**
104
     * @var array
105
     */
106
    protected $optionalParameters = [];
107
108
    /**
109
     * Constructor.
110
     *
111
     * @param string $name
112
     * @param array $params Array of parameters
113
     * @param array $rules
114
     * @param ConditionNode $condition
115
     * @param bool $variadic
116
     */
117
    public function __construct(
118
        $name,
119
        array $params = [],
120
        array $rules = [],
121
        $condition = null,
122
        $variadic = false,
123
        $frames = []
124
    ) {
125
        $this->name = $name;
126
        $this->selectors = [new SelectorNode([new ElementNode(null, $name)])];
127
128
        $this->params = $params;
129
        $this->condition = $condition;
130
        $this->variadic = (boolean) $variadic;
131
        $this->rules = $rules;
132
        $this->required = 0;
133
134
        if ($params) {
135
            $this->arity = count($params);
136
            foreach ($params as $p) {
137
                if (!isset($p['name']) || ($p['name'] && !isset($p['value']))) {
138
                    ++$this->required;
139
                } else {
140
                    $this->optionalParameters[] = $p['name'];
141
                }
142
            }
143
        }
144
145
        $this->frames = $frames;
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151
    public function accept(VisitorInterface $visitor)
152
    {
153
        if ($this->params) {
154
            $this->params = $visitor->visitArray($this->params);
0 ignored issues
show
Documentation Bug introduced by
It seems like $visitor->visitArray($this->params) of type * is incompatible with the declared type array of property $params.

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...
155
        }
156
157
        $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...
158
159
        if ($this->condition) {
160
            $this->condition = $visitor->visit($this->condition);
161
        }
162
    }
163
164
    /**
165
     * Compiles the node.
166
     *
167
     * @param Context $context The context
168
     * @param array|null $arguments Array of arguments
169
     * @param bool|null $important Important flag
170
     *
171
     * @return MixinDefinitionNode
172
     */
173
    public function compile(Context $context, $arguments = null, $important = null)
174
    {
175
        return new self(
176
            $this->name,
177
            $this->params,
178
            $this->rules,
179
            $this->condition,
180
            $this->variadic,
181
            count($this->frames) ? $this->frames : $context->frames
182
        );
183
    }
184
185
    /**
186
     * {@inheritdoc}
187
     */
188
    public function compileCall(Context $context, $arguments = null, $important = null)
189
    {
190
        $mixinFrames = array_merge($this->frames, $context->frames);
191
        $mixinEnv = Context::createCopyForCompilation($context, $mixinFrames);
192
193
        $compiledArguments = [];
194
        $frame = $this->compileParams($context, $mixinEnv, (array) $arguments, $compiledArguments);
195
196
        // use array_values so the array keys are reset
197
        $ex = new ExpressionNode(array_values($compiledArguments));
198
        array_unshift($frame->rules, new RuleNode('@arguments', $ex->compile($context)));
199
200
        $ruleset = new RulesetNode([], $this->rules);
201
        $ruleset->originalRuleset = $this;
202
203
        $ruleSetEnv = Context::createCopyForCompilation($context, array_merge([$this, $frame], $mixinFrames));
204
        $ruleset = $ruleset->compile($ruleSetEnv);
205
206
        if ($important) {
207
            $ruleset = $ruleset->makeImportant();
208
        }
209
210
        return $ruleset;
211
    }
212
213
    /**
214
     * Compile parameters.
215
     *
216
     * @param Context $context The context
217
     * @param Context $mixinEnv The mixin environment
218
     * @param array $arguments Array of arguments
219
     * @param array $compiledArguments The compiled arguments
220
     *
221
     * @throws
222
     *
223
     * @return mixed
224
     */
225
    public function compileParams(
226
        Context $context,
227
        Context $mixinEnv,
228
        $arguments = [],
229
        array &$compiledArguments = []
230
    ) {
231
        $frame = new RulesetNode([], []);
232
        $params = $this->params;
233
        $argsCount = 0;
234
235
        if (isset($mixinEnv->frames[0]) && $mixinEnv->frames[0]->functionRegistry) {
236
            $frame->functionRegistry = $mixinEnv->frames[0]->functionRegistry->inherit();
237
        }
238
239
        // create a copy of mixin environment
240
        $mixinEnv = Context::createCopyForCompilation($mixinEnv, array_merge([$frame], $mixinEnv->frames));
241
242
        if ($arguments) {
243
            $argsCount = count($arguments);
244
            for ($i = 0; $i < $argsCount; ++$i) {
245
                if (!isset($arguments[$i])) {
246
                    continue;
247
                }
248
                $arg = $arguments[$i];
249
                if (isset($arg['name']) && $name = $arg['name']) {
250
                    $isNamedFound = false;
251
                    foreach ($params as $j => $param) {
252
                        if (!isset($compiledArguments[$j]) && $name === $params[$j]['name']) {
253
                            $compiledArguments[$j] = $arg['value']->compile($context);
254
                            array_unshift($frame->rules, new RuleNode($name, $arg['value']->compile($context)));
255
                            $isNamedFound = true;
256
                            break;
257
                        }
258
                    }
259
                    if ($isNamedFound) {
260
                        array_splice($arguments, $i, 1);
261
                        --$i;
262
                        continue;
263
                    } else {
264
                        throw new CompilerException(sprintf('The named argument for `%s` %s was not found.',
265
                            $this->name, $arguments[$i]['name']));
266
                    }
267
                }
268
            }
269
        }
270
271
        $argIndex = 0;
272
        foreach ($params as $i => $param) {
273
            if (array_key_exists($i, $compiledArguments)) {
274
                continue;
275
            }
276
            $arg = null;
277
            if (array_key_exists($argIndex, $arguments)) {
278
                $arg = $arguments[$argIndex];
279
            }
280
            if (isset($param['name']) && ($name = $param['name'])) {
281
                if (isset($param['variadic']) && $param['variadic']) {
282
                    $varArgs = [];
283
                    for ($j = $argIndex; $j < $argsCount; ++$j) {
284
                        $varArgs[] = $arguments[$j]['value']->compile($context);
285
                    }
286
                    $expression = new ExpressionNode($varArgs);
287
                    array_unshift($frame->rules, new RuleNode($name, $expression->compile($context)));
288
                } else {
289
                    $val = ($arg && $arg['value']) ? $arg['value'] : false;
290
                    if ($val) {
291
                        $val = $val->compile($context);
292
                    } elseif (isset($param['value'])) {
293
                        $val = $param['value']->compile($mixinEnv);
294
                        $frame->resetCache();
295
                    } else {
296
                        throw new CompilerException(
297
                            sprintf('Wrong number of arguments for `%s` (%s for %s)',
298
                                $this->name, count($arguments), $this->arity)
299
                        );
300
                    }
301
302
                    array_unshift($frame->rules, new RuleNode($name, $val));
303
                    $compiledArguments[$i] = $val;
304
                }
305
            }
306
307
            if (isset($param['variadic']) && $param['variadic'] && $arguments) {
308
                for ($j = $argIndex; $j < $argsCount; ++$j) {
309
                    $compiledArguments[$j] = $arguments[$j]['value']->compile($context);
310
                }
311
            }
312
            ++$argIndex;
313
        }
314
315
        ksort($compiledArguments);
316
317
        return $frame;
318
    }
319
320
    /**
321
     * Match a condition.
322
     *
323
     * @param array $arguments
324
     * @param Context $context
325
     *
326
     * @return bool
327
     */
328
    public function matchCondition(array $arguments, Context $context)
329
    {
330
        if (!$this->condition) {
331
            return true;
332
        }
333
334
        $frame = $this->compileParams(
335
            $context,
336
            Context::createCopyForCompilation($context, array_merge($this->frames, $context->frames)),
337
            $arguments
338
        );
339
340
        $compileEnv = Context::createCopyForCompilation($context, array_merge(
341
            [$frame], $this->frames, $context->frames
342
        ));
343
344
        if (!$this->condition->compile($compileEnv)) {
345
            return false;
346
        }
347
348
        return true;
349
    }
350
351
    /**
352
     * Matches arguments.
353
     *
354
     * @param array $args
355
     * @param Context $context
356
     *
357
     * @return bool
358
     */
359
    public function matchArgs(array $args, Context $context)
360
    {
361
        $argsLength = count($args);
362
363
        $requiredArgsCount = 0;
364
        foreach ($args as $arg) {
365
            if (!isset($arg['name']) || !in_array($arg['name'], $this->optionalParameters)) {
366
                ++$requiredArgsCount;
367
            }
368
        }
369
370
        if (!$this->variadic) {
371
            if ($requiredArgsCount < $this->required) {
372
                return false;
373
            }
374
            if ($argsLength > count($this->params)) {
375
                return false;
376
            }
377
        } else {
378
            if ($requiredArgsCount < ($this->required - 1)) {
379
                return false;
380
            }
381
        }
382
383
        $len = min($requiredArgsCount, $this->arity);
384
385
        for ($i = 0; $i < $len; ++$i) {
386
            if (!isset($this->params[$i]['name']) && !isset($this->params[$i]['variadic'])) {
387
                if ($args[$i]['value']->compile($context)->toCSS($context) != $this->params[$i]['value']->compile($context)->toCSS($context)) {
388
                    return false;
389
                }
390
            }
391
        }
392
393
        return true;
394
    }
395
396
    /**
397
     * Returns ruleset with nodes marked as important.
398
     *
399
     * @return MixinDefinitionNode
400
     */
401
    public function makeImportant()
402
    {
403
        $importantRules = [];
404
        foreach ($this->rules as $rule) {
405
            if ($rule instanceof MakeableImportantInterface) {
406
                $importantRules[] = $rule->makeImportant();
407
            } else {
408
                $importantRules[] = $rule;
409
            }
410
        }
411
412
        return new self($this->name, $this->params, $importantRules, $this->condition, $this->variadic,
413
            $this->frames);
414
    }
415
}
416