Passed
Push — master ( cfdd1e...2b821f )
by Marcus
02:49
created

Compiler::compileProp()   F

Complexity

Conditions 34
Paths 6298

Size

Total Lines 145
Code Lines 101

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 92
CRAP Score 34.0837

Importance

Changes 0
Metric Value
dl 0
loc 145
ccs 92
cts 96
cp 0.9583
rs 2
c 0
b 0
f 0
cc 34
eloc 101
nc 6298
nop 3
crap 34.0837

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace LesserPhp;
4
5
/**
6
 * lesserphp
7
 * https://www.maswaba.de/lesserphp
8
 *
9
 * LESS CSS compiler, adapted from http://lesscss.org
10
 *
11
 * Copyright 2013, Leaf Corcoran <[email protected]>
12
 * Copyright 2016, Marcus Schwarz <[email protected]>
13
 * Licensed under MIT or GPLv3, see LICENSE
14
 * @package LesserPhp
15
 */
16
use LesserPhp\Color\Converter;
17
use LesserPhp\Exception\GeneralException;
18
use LesserPhp\Library\Assertions;
19
use LesserPhp\Library\Coerce;
20
use LesserPhp\Library\Functions;
21
22
/**
23
 * The LESS compiler and parser.
24
 *
25
 * Converting LESS to CSS is a three stage process. The incoming file is parsed
26
 * by `lessc_parser` into a syntax tree, then it is compiled into another tree
27
 * representing the CSS structure by `lessc`. The CSS tree is fed into a
28
 * formatter, like `lessc_formatter` which then outputs CSS as a string.
29
 *
30
 * During the first compile, all values are *reduced*, which means that their
31
 * types are brought to the lowest form before being dump as strings. This
32
 * handles math equations, variable dereferences, and the like.
33
 *
34
 * The `parse` function of `lessc` is the entry point.
35
 *
36
 * In summary:
37
 *
38
 * The `lessc` class creates an instance of the parser, feeds it LESS code,
39
 * then transforms the resulting tree to a CSS tree. This class also holds the
40
 * evaluation context, such as all available mixins and variables at any given
41
 * time.
42
 *
43
 * The `lessc_parser` class is only concerned with parsing its input.
44
 *
45
 * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string,
46
 * handling things like indentation.
47
 */
48
class Compiler
0 ignored issues
show
Coding Style introduced by
The property $lengths_to_base is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
49
{
50
51
    static public $VERSION = "v0.5.1";
52
53
    static public $TRUE = ["keyword", "true"];
54
    static public $FALSE = ["keyword", "false"];
55
56
    protected $libFunctions = [];
57
    protected $registeredVars = [];
58
    protected $preserveComments = false;
59
60
    public $vPrefix = '@'; // prefix of abstract properties
61
    public $mPrefix = '$'; // prefix of abstract blocks
62
    public $parentSelector = '&';
63
64
    static public $lengths = ["px", "m", "cm", "mm", "in", "pt", "pc"];
65
    static public $times = ["s", "ms"];
66
    static public $angles = ["rad", "deg", "grad", "turn"];
67
68
    static public $lengths_to_base = [1, 3779.52755906, 37.79527559, 3.77952756, 96, 1.33333333, 16];
69
    public $importDisabled = false;
70
    public $importDir = [];
71
72
    protected $numberPrecision;
73
74
    protected $allParsedFiles = [];
75
76
    // set to the parser that generated the current line when compiling
77
    // so we know how to create error messages
78
    /**
79
     * @var \LesserPhp\Parser
80
     */
81
    protected $sourceParser;
82
    protected $sourceLoc;
83
84
    static protected $nextImportId = 0; // uniquely identify imports
85
86
    /** @var \LesserPhp\Parser */
87
    private $parser;
88
    /** @var \LesserPhp\Formatter\FormatterInterface */
89
    private $formatter;
90
    /**
91
     * @var \LesserPhp\NodeEnv
92
     */
93
    private $env;
94
95
    /**
96
     * @var \LesserPhp\Library\Coerce
97
     */
98
    private $coerce;
99
    /**
100
     * @var \LesserPhp\Library\Assertions
101
     */
102
    private $assertions;
103
    /**
104
     * @var \LesserPhp\Library\Functions
105
     */
106
    private $functions;
107
108
    /**
109
     * @var mixed what's this exactly?
110
     */
111
    private $scope;
112
    /**
113
     * @var string
114
     */
115
    private $formatterName;
116
117
    /**
118
     * Initialize any static state, can initialize parser for a file
119
     * $opts isn't used yet
120
     */
121 50
    public function __construct($fname = null)
122
    {
123 50
        if ($fname !== null) {
124
            // used for deprecated parse method
125 1
            $this->_parseFile = $fname;
0 ignored issues
show
Bug introduced by
The property _parseFile does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
126
        }
127
128 50
        $this->coerce = new Coerce();
129 50
        $this->assertions = new Assertions($this->coerce);
130 50
        $this->converter = new Converter();
0 ignored issues
show
Bug introduced by
The property converter does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
131 50
        $this->functions = new Functions($this->assertions, $this->coerce, $this, $this->converter);
132 50
    }
133
134
    // attempts to find the path of an import url, returns null for css files
135 3 View Code Duplication
    protected function findImport($url)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
136
    {
137 3
        foreach ((array)$this->importDir as $dir) {
138 3
            $full = $dir . (substr($dir, -1) !== '/' ? '/' : '') . $url;
139 3
            if ($this->fileExists($file = $full . '.less') || $this->fileExists($file = $full)) {
140 3
                return $file;
141
            }
142
        }
143
144 2
        return null;
145
    }
146
147 3
    protected function fileExists($name)
148
    {
149 3
        return is_file($name);
150
    }
151
152 49
    public static function compressList($items, $delim)
153
    {
154 49
        if (!isset($items[1]) && isset($items[0])) {
155 49
            return $items[0];
156
        } else {
157 27
            return ['list', $delim, $items];
158
        }
159
    }
160
161 36
    public static function pregQuote($what)
162
    {
163 36
        return preg_quote($what, '/');
164
    }
165
166 3
    protected function tryImport($importPath, $parentBlock, $out)
167
    {
168 3
        if ($importPath[0] === 'function' && $importPath[1] === 'url') {
169 2
            $importPath = $this->flattenList($importPath[2]);
170
        }
171
172 3
        $str = $this->coerce->coerceString($importPath);
173 3
        if ($str === null) {
174 2
            return false;
175
        }
176
177 3
        $url = $this->compileValue($this->functions->e($str));
178
179
        // don't import if it ends in css
180 3
        if (substr_compare($url, '.css', -4, 4) === 0) {
181
            return false;
182
        }
183
184 3
        $realPath = $this->findImport($url);
185
186 3
        if ($realPath === null) {
187 2
            return false;
188
        }
189
190 3
        if ($this->importDisabled) {
191 1
            return [false, '/* import disabled */'];
192
        }
193
194 2
        if (isset($this->allParsedFiles[realpath($realPath)])) {
195 2
            return [false, null];
196
        }
197
198 2
        $this->addParsedFile($realPath);
199 2
        $parser = $this->makeParser($realPath);
200 2
        $root = $parser->parse(file_get_contents($realPath));
201
202
        // set the parents of all the block props
203 2
        foreach ($root->props as $prop) {
204 2
            if ($prop[0] === 'block') {
205 2
                $prop[1]->parent = $parentBlock;
206
            }
207
        }
208
209
        // copy mixins into scope, set their parents
210
        // bring blocks from import into current block
211
        // TODO: need to mark the source parser	these came from this file
212 2
        foreach ($root->children as $childName => $child) {
213 2
            if (isset($parentBlock->children[$childName])) {
214 2
                $parentBlock->children[$childName] = array_merge(
215 2
                    $parentBlock->children[$childName],
216
                    $child
217
                );
218
            } else {
219 2
                $parentBlock->children[$childName] = $child;
220
            }
221
        }
222
223 2
        $pi = pathinfo($realPath);
224 2
        $dir = $pi["dirname"];
225
226 2
        list($top, $bottom) = $this->sortProps($root->props, true);
227 2
        $this->compileImportedProps($top, $parentBlock, $out, $dir);
228
229 2
        return [true, $bottom, $parser, $dir];
230
    }
231
232 2
    protected function compileImportedProps($props, $block, $out, $importDir)
233
    {
234 2
        $oldSourceParser = $this->sourceParser;
235
236 2
        $oldImport = $this->importDir;
237
238
        // TODO: this is because the importDir api is stupid
239 2
        $this->importDir = (array)$this->importDir;
240 2
        array_unshift($this->importDir, $importDir);
241
242 2
        foreach ($props as $prop) {
243 2
            $this->compileProp($prop, $block, $out);
244
        }
245
246 2
        $this->importDir = $oldImport;
247 2
        $this->sourceParser = $oldSourceParser;
248 2
    }
249
250
    /**
251
     * Recursively compiles a block.
252
     *
253
     * A block is analogous to a CSS block in most cases. A single LESS document
254
     * is encapsulated in a block when parsed, but it does not have parent tags
255
     * so all of it's children appear on the root level when compiled.
256
     *
257
     * Blocks are made up of props and children.
258
     *
259
     * Props are property instructions, array tuples which describe an action
260
     * to be taken, eg. write a property, set a variable, mixin a block.
261
     *
262
     * The children of a block are just all the blocks that are defined within.
263
     * This is used to look up mixins when performing a mixin.
264
     *
265
     * Compiling the block involves pushing a fresh environment on the stack,
266
     * and iterating through the props, compiling each one.
267
     *
268
     * See lessc::compileProp()
269
     *
270
     * @param $block
271
     */
272 50
    protected function compileBlock($block)
273
    {
274 50
        switch ($block->type) {
275 50
            case "root":
276 50
                $this->compileRoot($block);
277 39
                break;
278 47
            case null:
279 47
                $this->compileCSSBlock($block);
280 36
                break;
281 6
            case "media":
282 3
                $this->compileMedia($block);
283 3
                break;
284 4
            case "directive":
285 4
                $name = "@" . $block->name;
286 4
                if (!empty($block->value)) {
287 2
                    $name .= " " . $this->compileValue($this->reduce($block->value));
288
                }
289
290 4
                $this->compileNestedBlock($block, [$name]);
291 4
                break;
292
            default:
293
                $block->parser->throwError("unknown block type: $block->type\n", $block->count);
294
        }
295 39
    }
296
297 47
    protected function compileCSSBlock($block)
298
    {
299 47
        $env = $this->pushEnv($this->env);
300
301 47
        $selectors = $this->compileSelectors($block->tags);
302 47
        $env->setSelectors($this->multiplySelectors($selectors));
303 47
        $out = $this->makeOutputBlock(null, $env->getSelectors());
304
305 47
        $this->scope->children[] = $out;
306 47
        $this->compileProps($block, $out);
307
308 36
        $block->scope = $env; // mixins carry scope with them!
309 36
        $this->popEnv();
310 36
    }
311
312 3
    protected function compileMedia($media)
313
    {
314 3
        $env = $this->pushEnv($this->env, $media);
315 3
        $parentScope = $this->mediaParent($this->scope);
316
317 3
        $query = $this->compileMediaQuery($this->multiplyMedia($env));
318
319 3
        $this->scope = $this->makeOutputBlock($media->type, [$query]);
320 3
        $parentScope->children[] = $this->scope;
321
322 3
        $this->compileProps($media, $this->scope);
323
324 3
        if (count($this->scope->lines) > 0) {
325 3
            $orphanSelelectors = $this->findClosestSelectors();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $orphanSelelectors is correct as $this->findClosestSelectors() (which targets LesserPhp\Compiler::findClosestSelectors()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
326 3
            if ($orphanSelelectors !== null) {
327 3
                $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
328 3
                $orphan->lines = $this->scope->lines;
329 3
                array_unshift($this->scope->children, $orphan);
330 3
                $this->scope->lines = [];
331
            }
332
        }
333
334 3
        $this->scope = $this->scope->parent;
335 3
        $this->popEnv();
336 3
    }
337
338 3
    protected function mediaParent($scope)
339
    {
340 3
        while (!empty($scope->parent)) {
341 1
            if (!empty($scope->type) && $scope->type !== "media") {
342 1
                break;
343
            }
344 1
            $scope = $scope->parent;
345
        }
346
347 3
        return $scope;
348
    }
349
350 4
    protected function compileNestedBlock($block, $selectors)
351
    {
352 4
        $this->pushEnv($this->env, $block);
353 4
        $this->scope = $this->makeOutputBlock($block->type, $selectors);
354 4
        $this->scope->parent->children[] = $this->scope;
355
356 4
        $this->compileProps($block, $this->scope);
357
358 4
        $this->scope = $this->scope->parent;
359 4
        $this->popEnv();
360 4
    }
361
362 50
    protected function compileRoot($root)
363
    {
364 50
        $this->pushEnv($this->env);
365 50
        $this->scope = $this->makeOutputBlock($root->type);
366 50
        $this->compileProps($root, $this->scope);
367 39
        $this->popEnv();
368 39
    }
369
370 50
    protected function compileProps($block, $out)
371
    {
372 50
        foreach ($this->sortProps($block->props) as $prop) {
373 50
            $this->compileProp($prop, $block, $out);
374
        }
375 39
        $out->lines = $this->deduplicate($out->lines);
376 39
    }
377
378
    /**
379
     * Deduplicate lines in a block. Comments are not deduplicated. If a
380
     * duplicate rule is detected, the comments immediately preceding each
381
     * occurence are consolidated.
382
     *
383
     * @param array $lines
384
     *
385
     * @return array
386
     */
387 39
    protected function deduplicate(array $lines)
388
    {
389 39
        $unique = [];
390 39
        $comments = [];
391
392 39
        foreach ($lines as $line) {
393 39
            if (strpos($line, '/*') === 0) {
394 2
                $comments[] = $line;
395 2
                continue;
396
            }
397 38
            if (!in_array($line, $unique)) {
398 38
                $unique[] = $line;
399
            }
400 38
            array_splice($unique, array_search($line, $unique), 0, $comments);
401 38
            $comments = [];
402
        }
403
404 39
        return array_merge($unique, $comments);
405
    }
406
407 50
    protected function sortProps(array $props, $split = false)
408
    {
409 50
        $vars = [];
410 50
        $imports = [];
411 50
        $other = [];
412 50
        $stack = [];
413
414 50
        foreach ($props as $prop) {
415 50
            switch ($prop[0]) {
416 50
                case "comment":
417 1
                    $stack[] = $prop;
418 1
                    break;
419 50
                case "assign":
420 44
                    $stack[] = $prop;
421 44
                    if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
422 23
                        $vars = array_merge($vars, $stack);
423
                    } else {
424 44
                        $other = array_merge($other, $stack);
425
                    }
426 44
                    $stack = [];
427 44
                    break;
428 48
                case "import":
429 3
                    $id = self::$nextImportId++;
430 3
                    $prop[] = $id;
431 3
                    $stack[] = $prop;
432 3
                    $imports = array_merge($imports, $stack);
433 3
                    $other[] = ["import_mixin", $id];
434 3
                    $stack = [];
435 3
                    break;
436
                default:
437 47
                    $stack[] = $prop;
438 47
                    $other = array_merge($other, $stack);
439 47
                    $stack = [];
440 50
                    break;
441
            }
442
        }
443 50
        $other = array_merge($other, $stack);
444
445 50
        if ($split) {
446 2
            return [array_merge($imports, $vars), $other];
447
        } else {
448 50
            return array_merge($imports, $vars, $other);
449
        }
450
    }
451
452 3
    protected function compileMediaQuery(array $queries)
453
    {
454 3
        $compiledQueries = [];
455 3
        foreach ($queries as $query) {
456 3
            $parts = [];
457 3
            foreach ($query as $q) {
458 3
                switch ($q[0]) {
459 3
                    case "mediaType":
460 3
                        $parts[] = implode(" ", array_slice($q, 1));
461 3
                        break;
462 1
                    case "mediaExp":
463 1
                        if (isset($q[2])) {
464 1
                            $parts[] = "($q[1]: " .
465 1
                                $this->compileValue($this->reduce($q[2])) . ")";
466
                        } else {
467 1
                            $parts[] = "($q[1])";
468
                        }
469 1
                        break;
470 1
                    case "variable":
471 1
                        $parts[] = $this->compileValue($this->reduce($q));
472 3
                        break;
473
                }
474
            }
475
476 3
            if (count($parts) > 0) {
477 3
                $compiledQueries[] = implode(" and ", $parts);
478
            }
479
        }
480
481 3
        $out = "@media";
482 3
        if (!empty($parts)) {
483
            $out .= " " .
484 3
                implode($this->formatter->getSelectorSeparator(), $compiledQueries);
485
        }
486
487 3
        return $out;
488
    }
489
490 3
    protected function multiplyMedia(NodeEnv $env = null, array $childQueries = null)
491
    {
492 3
        if (is_null($env) ||
493 3
            (!empty($env->getBlock()->type) && $env->getBlock()->type !== 'media')
494
        ) {
495 3
            return $childQueries;
496
        }
497
498
        // plain old block, skip
499 3
        if (empty($env->getBlock()->type)) {
500 3
            return $this->multiplyMedia($env->getParent(), $childQueries);
501
        }
502
503 3
        $out = [];
504 3
        $queries = $env->getBlock()->queries;
505 3
        if ($childQueries === null) {
506 3
            $out = $queries;
507
        } else {
508 1
            foreach ($queries as $parent) {
509 1
                foreach ($childQueries as $child) {
510 1
                    $out[] = array_merge($parent, $child);
511
                }
512
            }
513
        }
514
515 3
        return $this->multiplyMedia($env->getParent(), $out);
516
    }
517
518 47
    protected function expandParentSelectors(&$tag, $replace)
519
    {
520 47
        $parts = explode("$&$", $tag);
521 47
        $count = 0;
522 47
        foreach ($parts as &$part) {
523 47
            $part = str_replace($this->parentSelector, $replace, $part, $c);
524 47
            $count += $c;
525
        }
526 47
        $tag = implode($this->parentSelector, $parts);
527
528 47
        return $count;
529
    }
530
531 47
    protected function findClosestSelectors()
532
    {
533 47
        $env = $this->env;
534 47
        $selectors = null;
535 47
        while ($env !== null) {
536 47
            if ($env->getSelectors() !== null) {
537 13
                $selectors = $env->getSelectors();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $selectors is correct as $env->getSelectors() (which targets LesserPhp\NodeEnv::getSelectors()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
538 13
                break;
539
            }
540 47
            $env = $env->getParent();
541
        }
542
543 47
        return $selectors;
544
    }
545
546
547
    // multiply $selectors against the nearest selectors in env
548 47
    protected function multiplySelectors(array $selectors)
549
    {
550
        // find parent selectors
551
552 47
        $parentSelectors = $this->findClosestSelectors();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $parentSelectors is correct as $this->findClosestSelectors() (which targets LesserPhp\Compiler::findClosestSelectors()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
553 47
        if ($parentSelectors === null) {
554
            // kill parent reference in top level selector
555 47
            foreach ($selectors as &$s) {
556 47
                $this->expandParentSelectors($s, "");
557
            }
558
559 47
            return $selectors;
560
        }
561
562 13
        $out = [];
563 13
        foreach ($parentSelectors as $parent) {
0 ignored issues
show
Bug introduced by
The expression $parentSelectors of type null is not traversable.
Loading history...
564 13
            foreach ($selectors as $child) {
565 13
                $count = $this->expandParentSelectors($child, $parent);
566
567
                // don't prepend the parent tag if & was used
568 13
                if ($count > 0) {
569 4
                    $out[] = trim($child);
570
                } else {
571 13
                    $out[] = trim($parent . ' ' . $child);
572
                }
573
            }
574
        }
575
576 13
        return $out;
577
    }
578
579
    // reduces selector expressions
580 47
    protected function compileSelectors(array $selectors)
581
    {
582 47
        $out = [];
583
584 47
        foreach ($selectors as $s) {
585 47
            if (is_array($s)) {
586 4
                list(, $value) = $s;
587 4
                $out[] = trim($this->compileValue($this->reduce($value)));
588
            } else {
589 47
                $out[] = $s;
590
            }
591
        }
592
593 47
        return $out;
594
    }
595
596
    /**
597
     * @param $left
598
     * @param $right
599
     *
600
     * @return bool
601
     */
602 4
    protected function eq($left, $right)
603
    {
604 4
        return $left == $right;
605
    }
606
607 21
    protected function patternMatch($block, $orderedArgs, $keywordArgs)
608
    {
609
        // match the guards if it has them
610
        // any one of the groups must have all its guards pass for a match
611 21
        if (!empty($block->guards)) {
612 5
            $groupPassed = false;
613 5
            foreach ($block->guards as $guardGroup) {
614 5
                foreach ($guardGroup as $guard) {
615 5
                    $this->pushEnv($this->env);
616 5
                    $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
617
618 5
                    $negate = false;
619 5
                    if ($guard[0] === "negate") {
620 1
                        $guard = $guard[1];
621 1
                        $negate = true;
622
                    }
623
624 5
                    $passed = $this->reduce($guard) == self::$TRUE;
625 5
                    if ($negate) {
626 1
                        $passed = !$passed;
627
                    }
628
629 5
                    $this->popEnv();
630
631 5
                    if ($passed) {
632 3
                        $groupPassed = true;
633
                    } else {
634 5
                        $groupPassed = false;
635 5
                        break;
636
                    }
637
                }
638
639 5
                if ($groupPassed) {
640 5
                    break;
641
                }
642
            }
643
644 5
            if (!$groupPassed) {
645 5
                return false;
646
            }
647
        }
648
649 19
        if (empty($block->args)) {
650 12
            return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
651
        }
652
653 14
        $remainingArgs = $block->args;
654 14
        if ($keywordArgs) {
655 2
            $remainingArgs = [];
656 2
            foreach ($block->args as $arg) {
657 2
                if ($arg[0] === "arg" && isset($keywordArgs[$arg[1]])) {
658 2
                    continue;
659
                }
660
661 2
                $remainingArgs[] = $arg;
662
            }
663
        }
664
665 14
        $i = -1; // no args
666
        // try to match by arity or by argument literal
667 14
        foreach ($remainingArgs as $i => $arg) {
668 14
            switch ($arg[0]) {
669 14
                case "lit":
670 3
                    if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) {
671 2
                        return false;
672
                    }
673 3
                    break;
674 14
                case "arg":
675
                    // no arg and no default value
676 14
                    if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
677 3
                        return false;
678
                    }
679 14
                    break;
680 2
                case "rest":
681 2
                    $i--; // rest can be empty
682 14
                    break 2;
683
            }
684
        }
685
686 13
        if ($block->isVararg) {
687 2
            return true; // not having enough is handled above
688
        } else {
689 13
            $numMatched = $i + 1;
690
691
            // greater than because default values always match
692 13
            return $numMatched >= count($orderedArgs);
693
        }
694
    }
695
696 21
    protected function patternMatchAll(array $blocks, $orderedArgs, $keywordArgs, array $skip = [])
697
    {
698 21
        $matches = null;
699 21
        foreach ($blocks as $block) {
700
            // skip seen blocks that don't have arguments
701 21
            if (isset($skip[$block->id]) && !isset($block->args)) {
702 1
                continue;
703
            }
704
705 21
            if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
706 21
                $matches[] = $block;
707
            }
708
        }
709
710 21
        return $matches;
711
    }
712
713
    // attempt to find blocks matched by path and args
714 22
    protected function findBlocks($searchIn, array $path, $orderedArgs, $keywordArgs, array $seen = [])
715
    {
716 22
        if ($searchIn === null) {
717 5
            return null;
718
        }
719 22
        if (isset($seen[$searchIn->id])) {
720 1
            return null;
721
        }
722 22
        $seen[$searchIn->id] = true;
723
724 22
        $name = $path[0];
725
726 22
        if (isset($searchIn->children[$name])) {
727 21
            $blocks = $searchIn->children[$name];
728 21
            if (count($path) === 1) {
729 21
                $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $matches is correct as $this->patternMatchAll($...s, $keywordArgs, $seen) (which targets LesserPhp\Compiler::patternMatchAll()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
730 21
                if (!empty($matches)) {
731
                    // This will return all blocks that match in the closest
732
                    // scope that has any matching block, like lessjs
733 21
                    return $matches;
734
                }
735
            } else {
736 3
                $matches = [];
737 3
                foreach ($blocks as $subBlock) {
738 3
                    $subMatches = $this->findBlocks(
739
                        $subBlock,
740 3
                        array_slice($path, 1),
741
                        $orderedArgs,
742
                        $keywordArgs,
743
                        $seen
744
                    );
745
746 3
                    if ($subMatches !== null) {
747 3
                        foreach ($subMatches as $sm) {
748 3
                            $matches[] = $sm;
749
                        }
750
                    }
751
                }
752
753 3
                return count($matches) > 0 ? $matches : null;
754
            }
755
        }
756 22
        if ($searchIn->parent === $searchIn) {
757
            return null;
758
        }
759
760 22
        return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
761
    }
762
763
    // sets all argument names in $args to either the default value
764
    // or the one passed in through $values
765 16
    protected function zipSetArgs(array $args, $orderedValues, $keywordValues)
766
    {
767 16
        $assignedValues = [];
768
769 16
        $i = 0;
770 16
        foreach ($args as $a) {
771 14
            if ($a[0] === "arg") {
772 14
                if (isset($keywordValues[$a[1]])) {
773
                    // has keyword arg
774 2
                    $value = $keywordValues[$a[1]];
775 14
                } elseif (isset($orderedValues[$i])) {
776
                    // has ordered arg
777 13
                    $value = $orderedValues[$i];
778 13
                    $i++;
779 6
                } elseif (isset($a[2])) {
780
                    // has default value
781 6
                    $value = $a[2];
782
                } else {
783
                    throw new GeneralException("Failed to assign arg " . $a[1]);
784
                }
785
786 14
                $value = $this->reduce($value);
787 14
                $this->set($a[1], $value);
788 14
                $assignedValues[] = $value;
789
            } else {
790
                // a lit
791 14
                $i++;
792
            }
793
        }
794
795
        // check for a rest
796 16
        $last = end($args);
797 16
        if ($last[0] === "rest") {
798 2
            $rest = array_slice($orderedValues, count($args) - 1);
799 2
            $this->set($last[1], $this->reduce(["list", " ", $rest]));
800
        }
801
802
        // wow is this the only true use of PHP's + operator for arrays?
803 16
        $this->env->setArguments($assignedValues + $orderedValues);
804 16
    }
805
806
    // compile a prop and update $lines or $blocks appropriately
807 50
    protected function compileProp($prop, $block, $out)
808
    {
809
        // set error position context
810 50
        $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
811
812 50
        switch ($prop[0]) {
813 50
            case 'assign':
814 44
                list(, $name, $value) = $prop;
815 44
                if ($name[0] == $this->vPrefix) {
816 23
                    $this->set($name, $value);
817
                } else {
818 44
                    $out->lines[] = $this->formatter->property(
819
                        $name,
820 44
                        $this->compileValue($this->reduce($value))
821
                    );
822
                }
823 38
                break;
824 48
            case 'block':
825 47
                list(, $child) = $prop;
826 47
                $this->compileBlock($child);
827 36
                break;
828 25
            case 'ruleset':
829 25
            case 'mixin':
830 22
                list(, $path, $args, $suffix) = $prop;
831
832 22
                $orderedArgs = [];
833 22
                $keywordArgs = [];
834 22
                foreach ((array)$args as $arg) {
835 15
                    switch ($arg[0]) {
836 15
                        case "arg":
837 4
                            if (!isset($arg[2])) {
838 3
                                $orderedArgs[] = $this->reduce(["variable", $arg[1]]);
839
                            } else {
840 2
                                $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
841
                            }
842 4
                            break;
843
844 15
                        case "lit":
845 15
                            $orderedArgs[] = $this->reduce($arg[1]);
846 15
                            break;
847
                        default:
848 15
                            throw new GeneralException("Unknown arg type: " . $arg[0]);
849
                    }
850
                }
851
852 22
                $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
853
854 22
                if ($mixins === null) {
855 5
                    $block->parser->throwError("{$prop[1][0]} is undefined", $block->count);
856
                }
857
858 17
                if (strpos($prop[1][0], "$") === 0) {
859
                    //Use Ruleset Logic - Only last element
860 8
                    $mixins = [array_pop($mixins)];
861
                }
862
863 17
                foreach ($mixins as $mixin) {
864 17
                    if ($mixin === $block && !$orderedArgs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $orderedArgs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
865
                        continue;
866
                    }
867
868 17
                    $haveScope = false;
869 17
                    if (isset($mixin->parent->scope)) {
870 2
                        $haveScope = true;
871 2
                        $mixinParentEnv = $this->pushEnv($this->env);
872 2
                        $mixinParentEnv->storeParent = $mixin->parent->scope;
0 ignored issues
show
Bug introduced by
The property storeParent does not seem to exist. Did you mean parent?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
873
                    }
874
875 17
                    $haveArgs = false;
876 17
                    if (isset($mixin->args)) {
877 14
                        $haveArgs = true;
878 14
                        $this->pushEnv($this->env);
879 14
                        $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
880
                    }
881
882 17
                    $oldParent = $mixin->parent;
883 17
                    if ($mixin != $block) {
884 17
                        $mixin->parent = $block;
885
                    }
886
887 17
                    foreach ($this->sortProps($mixin->props) as $subProp) {
888 17
                        if ($suffix !== null &&
889 17
                            $subProp[0] === "assign" &&
890 17
                            is_string($subProp[1]) &&
891 17
                            $subProp[1]{0} != $this->vPrefix
892
                        ) {
893 1
                            $subProp[2] = [
894 1
                                'list',
895 1
                                ' ',
896 1
                                [$subProp[2], ['keyword', $suffix]],
897
                            ];
898
                        }
899
900 17
                        $this->compileProp($subProp, $mixin, $out);
901
                    }
902
903 17
                    $mixin->parent = $oldParent;
904
905 17
                    if ($haveArgs) {
906 14
                        $this->popEnv();
907
                    }
908 17
                    if ($haveScope) {
909 17
                        $this->popEnv();
910
                    }
911
                }
912
913 17
                break;
914 5
            case 'raw':
915
                $out->lines[] = $prop[1];
916
                break;
917 5
            case "directive":
918 1
                list(, $name, $value) = $prop;
919 1
                $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)) . ';';
920 1
                break;
921 4
            case "comment":
922 1
                $out->lines[] = $prop[1];
923 1
                break;
924 3
            case "import":
925 3
                list(, $importPath, $importId) = $prop;
926 3
                $importPath = $this->reduce($importPath);
927
928 3
                $result = $this->tryImport($importPath, $block, $out);
929
930 3
                $this->env->addImports($importId, $result === false ?
931 2
                    [false, "@import " . $this->compileValue($importPath) . ";"] :
932 3
                    $result);
933
934 3
                break;
935 3
            case "import_mixin":
936 3
                list(, $importId) = $prop;
937 3
                $import = $this->env->getImports($importId);
938 3
                if ($import[0] === false) {
939 3
                    if (isset($import[1])) {
940 3
                        $out->lines[] = $import[1];
941
                    }
942
                } else {
943 2
                    list(, $bottom, $parser, $importDir) = $import;
0 ignored issues
show
Unused Code introduced by
The assignment to $parser is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
944 2
                    $this->compileImportedProps($bottom, $block, $out, $importDir);
945
                }
946
947 3
                break;
948
            default:
949
                $block->parser->throwError("unknown op: {$prop[0]}\n", $block->count);
950
        }
951 39
    }
952
953
954
    /**
955
     * Compiles a primitive value into a CSS property value.
956
     *
957
     * Values in lessphp are typed by being wrapped in arrays, their format is
958
     * typically:
959
     *
960
     *     array(type, contents [, additional_contents]*)
961
     *
962
     * The input is expected to be reduced. This function will not work on
963
     * things like expressions and variables.
964
     *
965
     * @param array $value
966
     *
967
     * @return string
968
     * @throws \LesserPhp\Exception\GeneralException
969
     */
970 39
    public function compileValue(array $value)
971
    {
972 39
        switch ($value[0]) {
973 39
            case 'list':
974
                // [1] - delimiter
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
975
                // [2] - array of values
976 21
                return implode($value[1], array_map([$this, 'compileValue'], $value[2]));
977 39
            case 'raw_color':
978 8
                if ($this->formatter->getCompressColors()) {
979
                    return $this->compileValue($this->coerce->coerceColor($value));
980
                }
981
982 8
                return $value[1];
983 39
            case 'keyword':
984
                // [1] - the keyword
985 32
                return $value[1];
986 37
            case 'number':
987 32
                list(, $num, $unit) = $value;
988
                // [1] - the number
989
                // [2] - the unit
990 32
                if ($this->numberPrecision !== null) {
991
                    $num = round($num, $this->numberPrecision);
992
                }
993
994 32
                return $num . $unit;
995 30
            case 'string':
996
                // [1] - contents of string (includes quotes)
997 25
                list(, $delim, $content) = $value;
998 25
                foreach ($content as &$part) {
999 25
                    if (is_array($part)) {
1000 25
                        $part = $this->compileValue($part);
1001
                    }
1002
                }
1003
1004 25
                return $delim . implode($content) . $delim;
1005 17
            case 'color':
1006
                // [1] - red component (either number or a %)
1007
                // [2] - green component
1008
                // [3] - blue component
1009
                // [4] - optional alpha component
1010 8
                list(, $r, $g, $b) = $value;
1011 8
                $r = round($r);
1012 8
                $g = round($g);
1013 8
                $b = round($b);
1014
1015 8
                if (count($value) === 5 && $value[4] != 1) { // rgba
1016 3
                    return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $value[4] . ')';
1017
                }
1018
1019 8
                $h = sprintf("#%02x%02x%02x", $r, $g, $b);
1020
1021 8
                if ($this->formatter->getCompressColors()) {
1022
                    // Converting hex color to short notation (e.g. #003399 to #039)
1023 1
                    if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
1024 1
                        $h = '#' . $h[1] . $h[3] . $h[5];
1025
                    }
1026
                }
1027
1028 8
                return $h;
1029
1030 10
            case 'function':
1031 10
                list(, $name, $args) = $value;
1032
1033 10
                return $name . '(' . $this->compileValue($args) . ')';
1034
            default: // assumed to be unit
1035
                throw new GeneralException('unknown value type: ' . $value[0]);
1036
        }
1037
    }
1038
1039
    /**
1040
     * Helper function to get arguments for color manipulation functions.
1041
     * takes a list that contains a color like thing and a percentage
1042
     *
1043
     * @param array $args
1044
     *
1045
     * @return array
1046
     */
1047 2
    public function colorArgs(array $args)
1048
    {
1049 2
        if ($args[0] !== 'list' || count($args[2]) < 2) {
1050 1
            return [['color', 0, 0, 0], 0];
1051
        }
1052 2
        list($color, $delta) = $args[2];
1053 2
        $color = $this->assertions->assertColor($color);
1054 2
        $delta = (float)$delta[1];
1055
1056 2
        return [$color, $delta];
1057
    }
1058
1059
    /**
1060
     * Convert the rgb, rgba, hsl color literals of function type
1061
     * as returned by the parser into values of color type.
1062
     *
1063
     * @param array $func
1064
     *
1065
     * @return bool|mixed
1066
     */
1067 24
    protected function funcToColor(array $func)
1068
    {
1069 24
        $fname = $func[1];
1070 24
        if ($func[2][0] !== 'list') {
1071 6
            return false;
1072
        } // need a list of arguments
1073
        /** @var array $rawComponents */
1074 24
        $rawComponents = $func[2][2];
1075
1076 24
        if ($fname === 'hsl' || $fname === 'hsla') {
1077 1
            $hsl = ['hsl'];
1078 1
            $i = 0;
1079 1
            foreach ($rawComponents as $c) {
1080 1
                $val = $this->reduce($c);
1081 1
                $val = isset($val[1]) ? (float)$val[1] : 0;
1082
1083 1
                if ($i === 0) {
1084 1
                    $clamp = 360;
1085 1
                } elseif ($i < 3) {
1086 1
                    $clamp = 100;
1087
                } else {
1088 1
                    $clamp = 1;
1089
                }
1090
1091 1
                $hsl[] = $this->converter->clamp($val, $clamp);
1092 1
                $i++;
1093
            }
1094
1095 1
            while (count($hsl) < 4) {
1096
                $hsl[] = 0;
1097
            }
1098
1099 1
            return $this->converter->toRGB($hsl);
1100
1101 24
        } elseif ($fname === 'rgb' || $fname === 'rgba') {
1102 4
            $components = [];
1103 4
            $i = 1;
1104 4
            foreach ($rawComponents as $c) {
1105 4
                $c = $this->reduce($c);
1106 4
                if ($i < 4) {
1107 4 View Code Duplication
                    if ($c[0] === "number" && $c[2] === "%") {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1108 1
                        $components[] = 255 * ($c[1] / 100);
1109
                    } else {
1110 4
                        $components[] = (float)$c[1];
1111
                    }
1112 4 View Code Duplication
                } elseif ($i === 4) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1113 4
                    if ($c[0] === "number" && $c[2] === "%") {
1114
                        $components[] = 1.0 * ($c[1] / 100);
1115
                    } else {
1116 4
                        $components[] = (float)$c[1];
1117
                    }
1118
                } else {
1119
                    break;
1120
                }
1121
1122 4
                $i++;
1123
            }
1124 4
            while (count($components) < 3) {
1125
                $components[] = 0;
1126
            }
1127 4
            array_unshift($components, 'color');
1128
1129 4
            return $this->fixColor($components);
1130
        }
1131
1132 23
        return false;
1133
    }
1134
1135
    /**
1136
     * @param array $value
1137
     * @param bool  $forExpression
1138
     *
1139
     * @return array|bool|mixed|null // <!-- dafuq?
1140
     */
1141 49
    public function reduce(array $value, $forExpression = false)
1142
    {
1143 49
        switch ($value[0]) {
1144 49
            case "interpolate":
1145 7
                $reduced = $this->reduce($value[1]);
1146 7
                $var = $this->compileValue($reduced);
1147 7
                $res = $this->reduce(["variable", $this->vPrefix . $var]);
1148
1149 7
                if ($res[0] === "raw_color") {
1150 1
                    $res = $this->coerce->coerceColor($res);
1151
                }
1152
1153 7
                if (empty($value[2])) {
1154 6
                    $res = $this->functions->e($res);
1155
                }
1156
1157 7
                return $res;
1158 49
            case "variable":
1159 28
                $key = $value[1];
1160 28
                if (is_array($key)) {
1161 1
                    $key = $this->reduce($key);
1162 1
                    $key = $this->vPrefix . $this->compileValue($this->functions->e($key));
1163
                }
1164
1165 28
                $seen =& $this->env->seenNames;
1166
1167 28
                if (!empty($seen[$key])) {
1168
                    $this->throwError("infinite loop detected: $key");
1169
                }
1170
1171 28
                $seen[$key] = true;
1172 28
                $out = $this->reduce($this->get($key));
1173 27
                $seen[$key] = false;
1174
1175 27
                return $out;
1176 48
            case "list":
1177 29
                foreach ($value[2] as &$item) {
1178 26
                    $item = $this->reduce($item, $forExpression);
1179
                }
1180
1181 29
                return $value;
1182 48
            case "expression":
1183 19
                return $this->evaluate($value);
1184 48
            case "string":
1185 27
                foreach ($value[2] as &$part) {
1186 27
                    if (is_array($part)) {
1187 11
                        $strip = $part[0] === "variable";
1188 11
                        $part = $this->reduce($part);
1189 11
                        if ($strip) {
1190 27
                            $part = $this->functions->e($part);
1191
                        }
1192
                    }
1193
                }
1194
1195 27
                return $value;
1196 45
            case "escape":
1197 4
                list(, $inner) = $value;
1198
1199 4
                return $this->functions->e($this->reduce($inner));
1200 45
            case "function":
1201 24
                $color = $this->funcToColor($value);
1202 24
                if ($color) {
1203 4
                    return $color;
1204
                }
1205
1206 23
                list(, $name, $args) = $value;
1207 23
                if ($name === "%") {
1208 1
                    $name = "_sprintf";
1209
                }
1210
1211
                // user functions
1212 23
                $f = null;
1213 23
                if (isset($this->libFunctions[$name]) && is_callable($this->libFunctions[$name])) {
1214 1
                    $f = $this->libFunctions[$name];
1215
                }
1216
1217 23
                $func = str_replace('-', '_', $name);
1218
1219 23
                if ($f !== null || method_exists($this->functions, $func)) {
1220 14
                    if ($args[0] === 'list') {
1221 14
                        $args = self::compressList($args[2], $args[1]);
1222
                    }
1223
1224 14
                    if ($f !== null) {
1225 1
                        $ret = $f($this->reduce($args, true), $this);
1226
                    } else {
1227 13
                        $ret = $this->functions->$func($this->reduce($args, true), $this);
1228
                    }
1229 9
                    if ($ret === null) {
1230
                        return [
1231 2
                            "string",
1232 2
                            "",
1233
                            [
1234 2
                                $name,
1235 2
                                "(",
1236 2
                                $args,
1237 2
                                ")",
1238
                            ],
1239
                        ];
1240
                    }
1241
1242
                    // convert to a typed value if the result is a php primitive
1243 8
                    if (is_numeric($ret)) {
1244 3
                        $ret = ['number', $ret, ""];
1245 7
                    } elseif (!is_array($ret)) {
1246 2
                        $ret = ['keyword', $ret];
1247
                    }
1248
1249 8
                    return $ret;
1250
                }
1251
1252
                // plain function, reduce args
1253 10
                $value[2] = $this->reduce($value[2]);
1254
1255 10
                return $value;
1256 40
            case "unary":
1257 6
                list(, $op, $exp) = $value;
1258 6
                $exp = $this->reduce($exp);
1259
1260 6
                if ($exp[0] === "number") {
1261
                    switch ($op) {
1262 6
                        case "+":
1263
                            return $exp;
1264 6
                        case "-":
1265 6
                            $exp[1] *= -1;
1266
1267 6
                            return $exp;
1268
                    }
1269
                }
1270
1271
                return ["string", "", [$op, $exp]];
1272
        }
1273
1274 40
        if ($forExpression) {
1275 22
            switch ($value[0]) {
1276 22
                case "keyword":
1277 5
                    $color = $this->coerce->coerceColor($value);
1278 5
                    if ($color !== null) {
1279 2
                        return $color;
1280
                    }
1281 5
                    break;
1282 22
                case "raw_color":
1283 6
                    return $this->coerce->coerceColor($value);
1284
            }
1285
        }
1286
1287 40
        return $value;
1288
    }
1289
1290
1291
    // turn list of length 1 into value type
1292 2
    protected function flattenList($value)
1293
    {
1294 2
        if ($value[0] === "list" && count($value[2]) === 1) {
1295 2
            return $this->flattenList($value[2][0]);
1296
        }
1297
1298 2
        return $value;
1299
    }
1300
1301 5
    public function toBool($a)
1302
    {
1303 5
        if ($a) {
1304 4
            return self::$TRUE;
1305
        } else {
1306 5
            return self::$FALSE;
1307
        }
1308
    }
1309
1310
    // evaluate an expression
1311 19
    protected function evaluate($exp)
1312
    {
1313 19
        list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
1314
1315 19
        $left = $this->reduce($left, true);
1316 19
        $right = $this->reduce($right, true);
1317
1318 19
        $leftColor = $this->coerce->coerceColor($left);
1319 19
        if ($leftColor !== null) {
1320 5
            $left = $leftColor;
1321
        }
1322
1323 19
        $rightColor = $this->coerce->coerceColor($right);
1324 19
        if ($rightColor !== null) {
1325 5
            $right = $rightColor;
1326
        }
1327
1328 19
        $ltype = $left[0];
1329 19
        $rtype = $right[0];
1330
1331
        // operators that work on all types
1332 19
        if ($op === "and") {
1333
            return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
1334
        }
1335
1336 19
        if ($op === "=") {
1337 1
            return $this->toBool($this->eq($left, $right));
1338
        }
1339
1340 19
        $str = $this->stringConcatenate($left, $right);
1341 19
        if ($op === "+" && $str !== null) {
1342 2
            return $str;
1343
        }
1344
1345
        // type based operators
1346 17
        $fname = "op_${ltype}_${rtype}";
1347 17
        if (is_callable([$this, $fname])) {
1348 17
            $out = $this->$fname($op, $left, $right);
1349 17
            if ($out !== null) {
1350 17
                return $out;
1351
            }
1352
        }
1353
1354
        // make the expression look it did before being parsed
1355 1
        $paddedOp = $op;
1356 1
        if ($whiteBefore) {
1357 1
            $paddedOp = " " . $paddedOp;
1358
        }
1359 1
        if ($whiteAfter) {
1360 1
            $paddedOp .= " ";
1361
        }
1362
1363 1
        return ["string", "", [$left, $paddedOp, $right]];
1364
    }
1365
1366
    protected function stringConcatenate($left, $right)
1367
    {
1368 19
        $strLeft = $this->coerce->coerceString($left);
1369 19
        if ($strLeft !== null) {
1370 2
            if ($right[0] === "string") {
1371 2
                $right[1] = "";
1372
            }
1373 2
            $strLeft[2][] = $right;
1374
1375 2
            return $strLeft;
1376
        }
1377
1378 18
        $strRight = $this->coerce->coerceString($right);
1379 18
        if ($strRight !== null) {
1380 1
            array_unshift($strRight[2], $left);
1381
1382 1
            return $strRight;
1383
        }
1384
1385 17
        return null;
1386
    }
1387
1388
1389
    // make sure a color's components don't go out of bounds
1390
    public function fixColor($c)
1391
    {
1392 7
        foreach (range(1, 3) as $i) {
1393 7
            if ($c[$i] < 0) {
1394
                $c[$i] = 0;
1395
            }
1396 7
            if ($c[$i] > 255) {
1397 7
                $c[$i] = 255;
1398
            }
1399
        }
1400
1401 7
        return $c;
1402
    }
1403
1404
    protected function op_number_color($op, $lft, $rgt)
1405
    {
1406 1
        if ($op === '+' || $op === '*') {
1407 1
            return $this->op_color_number($op, $rgt, $lft);
1408
        }
1409
1410 1
        return null;
1411
    }
1412
1413
    protected function op_color_number($op, $lft, $rgt)
1414
    {
1415 2
        if ($rgt[0] === '%') {
1416
            $rgt[1] /= 100;
1417
        }
1418
1419 2
        return $this->op_color_color(
1420
            $op,
1421
            $lft,
1422 2
            array_fill(1, count($lft) - 1, $rgt[1])
1423
        );
1424
    }
1425
1426
    protected function op_color_color($op, $left, $right)
1427
    {
1428 5
        $out = ['color'];
1429 5
        $max = count($left) > count($right) ? count($left) : count($right);
1430 5
        foreach (range(1, $max - 1) as $i) {
1431 5
            $lval = isset($left[$i]) ? $left[$i] : 0;
1432 5
            $rval = isset($right[$i]) ? $right[$i] : 0;
1433
            switch ($op) {
1434 5
                case '+':
1435 5
                    $out[] = $lval + $rval;
1436 5
                    break;
1437 1
                case '-':
1438 1
                    $out[] = $lval - $rval;
1439 1
                    break;
1440 1
                case '*':
1441 1
                    $out[] = $lval * $rval;
1442 1
                    break;
1443 1
                case '%':
1444 1
                    $out[] = $lval % $rval;
1445 1
                    break;
1446 1
                case '/':
1447 1
                    if ($rval == 0) {
1448
                        throw new GeneralException("evaluate error: can't divide by zero");
1449
                    }
1450 1
                    $out[] = $lval / $rval;
1451 1
                    break;
1452
                default:
1453 5
                    throw new GeneralException('evaluate error: color op number failed on op ' . $op);
1454
            }
1455
        }
1456
1457 5
        return $this->fixColor($out);
1458
    }
1459
1460
1461
    // operator on two numbers
1462
    protected function op_number_number($op, $left, $right)
1463
    {
1464 15
        $unit = empty($left[2]) ? $right[2] : $left[2];
1465
1466 15
        $value = 0;
0 ignored issues
show
Unused Code introduced by
$value is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1467
        switch ($op) {
1468 15
            case '+':
1469 9
                $value = $left[1] + $right[1];
1470 9
                break;
1471 12
            case '*':
1472 8
                $value = $left[1] * $right[1];
1473 8
                break;
1474 10
            case '-':
1475 6
                $value = $left[1] - $right[1];
1476 6
                break;
1477 9
            case '%':
1478 1
                $value = $left[1] % $right[1];
1479 1
                break;
1480 8
            case '/':
1481 4
                if ($right[1] == 0) {
1482
                    throw new GeneralException('parse error: divide by zero');
1483
                }
1484 4
                $value = $left[1] / $right[1];
1485 4
                break;
1486 5
            case '<':
1487 1
                return $this->toBool($left[1] < $right[1]);
1488 5
            case '>':
1489 3
                return $this->toBool($left[1] > $right[1]);
1490 3
            case '>=':
1491 1
                return $this->toBool($left[1] >= $right[1]);
1492 2
            case '=<':
1493 2
                return $this->toBool($left[1] <= $right[1]);
1494
            default:
1495
                throw new GeneralException('parse error: unknown number operator: ' . $op);
1496
        }
1497
1498 13
        return ["number", $value, $unit];
1499
    }
1500
1501
1502
    /* environment functions */
1503
1504
    protected function makeOutputBlock($type, $selectors = null)
1505
    {
1506 50
        $b = new \stdClass();
1507 50
        $b->lines = [];
1508 50
        $b->children = [];
1509 50
        $b->selectors = $selectors;
1510 50
        $b->type = $type;
1511 50
        $b->parent = $this->scope;
1512
1513 50
        return $b;
1514
    }
1515
1516
    // the state of execution
1517
    protected function pushEnv($parent, $block = null)
1518
    {
1519 50
        $e = new \LesserPhp\NodeEnv();
1520 50
        $e->setParent($parent);
1521 50
        $e->setBlock($block);
1522 50
        $e->setStore([]);
1523
1524 50
        $this->env = $e;
1525
1526 50
        return $e;
1527
    }
1528
1529
    // pop something off the stack
1530
    protected function popEnv()
1531
    {
1532 41
        $old = $this->env;
1533 41
        $this->env = $this->env->getParent();
1534
1535 41
        return $old;
1536
    }
1537
1538
    // set something in the current env
1539
    protected function set($name, $value)
1540
    {
1541 28
        $this->env->addStore($name, $value);
1542 28
    }
1543
1544
1545
    // get the highest occurrence entry for a name
1546
    protected function get($name)
1547
    {
1548 28
        $current = $this->env;
1549
1550
        // track scope to evaluate
1551 28
        $scope_secondary = [];
1552
1553 28
        $isArguments = $name === $this->vPrefix . 'arguments';
1554 28
        while ($current) {
1555 28
            if ($isArguments && count($current->getArguments()) > 0) {
1556 3
                return ['list', ' ', $current->getArguments()];
1557
            }
1558
1559 28
            if (isset($current->getStore()[$name])) {
1560 27
                return $current->getStore()[$name];
1561
            }
1562
            // has secondary scope?
1563 19
            if (isset($current->storeParent)) {
1564
                $scope_secondary[] = $current->storeParent;
0 ignored issues
show
Bug introduced by
The property storeParent does not seem to exist. Did you mean parent?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1565
            }
1566
1567 19
            if ($current->getParent() !== null) {
1568 19
                $current = $current->getParent();
1569
            } else {
1570 1
                $current = null;
1571
            }
1572
        }
1573
1574 1
        while (count($scope_secondary)) {
1575
            // pop one off
1576
            $current = array_shift($scope_secondary);
1577
            while ($current) {
1578
                if ($isArguments && isset($current->arguments)) {
1579
                    return ['list', ' ', $current->arguments];
1580
                }
1581
1582
                if (isset($current->store[$name])) {
1583
                    return $current->store[$name];
1584
                }
1585
1586
                // has secondary scope?
1587
                if (isset($current->storeParent)) {
1588
                    $scope_secondary[] = $current->storeParent;
1589
                }
1590
1591
                if (isset($current->parent)) {
1592
                    $current = $current->parent;
1593
                } else {
1594
                    $current = null;
1595
                }
1596
            }
1597
        }
1598
1599 1
        throw new GeneralException("variable $name is undefined");
1600
    }
1601
1602
    // inject array of unparsed strings into environment as variables
1603
    protected function injectVariables(array $args)
1604
    {
1605 2
        $this->pushEnv($this->env);
1606 2
        $parser = new \LesserPhp\Parser($this, __METHOD__);
1607 2
        foreach ($args as $name => $strValue) {
1608 2
            if ($name{0} !== '@') {
1609 2
                $name = '@' . $name;
1610
            }
1611 2
            $parser->count = 0;
1612 2
            $parser->buffer = (string)$strValue;
1613 2
            if (!$parser->propertyValue($value)) {
1614
                throw new GeneralException("failed to parse passed in variable $name: $strValue");
1615
            }
1616
1617 2
            $this->set($name, $value);
1618
        }
1619 2
    }
1620
1621
    /**
1622
     * @param string $string
1623
     * @param string $name
1624
     *
1625
     * @return string
1626
     */
1627
    public function compile($string, $name = null)
1628
    {
1629 50
        $locale = setlocale(LC_NUMERIC, 0);
1630 50
        setlocale(LC_NUMERIC, 'C');
1631
1632 50
        $this->parser = $this->makeParser($name);
1633 50
        $root = $this->parser->parse($string);
1634
1635 50
        $this->env = null;
1636 50
        $this->scope = null;
1637 50
        $this->allParsedFiles = [];
1638
1639 50
        $this->formatter = $this->newFormatter();
1640
1641 50
        if (!empty($this->registeredVars)) {
1642 2
            $this->injectVariables($this->registeredVars);
1643
        }
1644
1645 50
        $this->sourceParser = $this->parser; // used for error messages
1646 50
        $this->compileBlock($root);
1647
1648 39
        ob_start();
1649 39
        $this->formatter->block($this->scope);
1650 39
        $out = ob_get_clean();
1651 39
        setlocale(LC_NUMERIC, $locale);
1652
1653 39
        return $out;
1654
    }
1655
1656
    public function compileFile($fname, $outFname = null)
1657
    {
1658 2
        if (!is_readable($fname)) {
1659
            throw new GeneralException('load error: failed to find ' . $fname);
1660
        }
1661
1662 2
        $pi = pathinfo($fname);
1663
1664 2
        $oldImport = $this->importDir;
1665
1666 2
        $this->importDir = (array)$this->importDir;
1667 2
        $this->importDir[] = $pi['dirname'] . '/';
1668
1669 2
        $this->addParsedFile($fname);
1670
1671 2
        $out = $this->compile(file_get_contents($fname), $fname);
1672
1673 2
        $this->importDir = $oldImport;
1674
1675 2
        if ($outFname !== null) {
1676
            return file_put_contents($outFname, $out);
1677
        }
1678
1679 2
        return $out;
1680
    }
1681
1682
    /**
1683
     * Based on explicit input/output files does a full change check on cache before compiling.
1684
     *
1685
     * @param string  $in
1686
     * @param string  $out
1687
     * @param boolean $force
1688
     *
1689
     * @return string Compiled CSS results
1690
     * @throws \Exception
1691
     */
1692
    public function checkedCachedCompile($in, $out, $force = false)
1693
    {
1694 1
        if (!is_file($in) || !is_readable($in)) {
1695
            throw new GeneralException('Invalid or unreadable input file specified.');
1696
        }
1697 1
        if (is_dir($out) || !is_writable(file_exists($out) ? $out : dirname($out))) {
1698
            throw new GeneralException('Invalid or unwritable output file specified.');
1699
        }
1700
1701 1
        $outMeta = $out . '.meta';
1702 1
        $metadata = null;
1703 1
        if (!$force && is_file($outMeta)) {
1704
            $metadata = unserialize(file_get_contents($outMeta));
1705
        }
1706
1707 1
        $output = $this->cachedCompile($metadata ?: $in);
1708
1709 1
        if (!$metadata || $metadata['updated'] != $output['updated']) {
1710 1
            $css = $output['compiled'];
1711 1
            unset($output['compiled']);
1712 1
            file_put_contents($out, $css);
1713 1
            file_put_contents($outMeta, serialize($output));
1714
        } else {
1715
            $css = file_get_contents($out);
1716
        }
1717
1718 1
        return $css;
1719
    }
1720
1721
    // compile only if changed input has changed or output doesn't exist
1722
    public function checkedCompile($in, $out)
1723
    {
1724
        if (!is_file($out) || filemtime($in) > filemtime($out)) {
1725
            $this->compileFile($in, $out);
1726
1727
            return true;
1728
        }
1729
1730
        return false;
1731
    }
1732
1733
    /**
1734
     * Execute lessphp on a .less file or a lessphp cache structure
1735
     *
1736
     * The lessphp cache structure contains information about a specific
1737
     * less file having been parsed. It can be used as a hint for future
1738
     * calls to determine whether or not a rebuild is required.
1739
     *
1740
     * The cache structure contains two important keys that may be used
1741
     * externally:
1742
     *
1743
     * compiled: The final compiled CSS
1744
     * updated: The time (in seconds) the CSS was last compiled
1745
     *
1746
     * The cache structure is a plain-ol' PHP associative array and can
1747
     * be serialized and unserialized without a hitch.
1748
     *
1749
     * @param mixed $in    Input
1750
     * @param bool  $force Force rebuild?
1751
     *
1752
     * @return array lessphp cache structure
1753
     */
1754
    public function cachedCompile($in, $force = false)
1755
    {
1756
        // assume no root
1757 1
        $root = null;
1758
1759 1
        if (is_string($in)) {
1760 1
            $root = $in;
1761
        } elseif (is_array($in) && isset($in['root'])) {
1762
            if ($force || !isset($in['files'])) {
1763
                // If we are forcing a recompile or if for some reason the
1764
                // structure does not contain any file information we should
1765
                // specify the root to trigger a rebuild.
1766
                $root = $in['root'];
1767
            } elseif (isset($in['files']) && is_array($in['files'])) {
1768
                foreach ($in['files'] as $fname => $ftime) {
1769
                    if (!file_exists($fname) || filemtime($fname) > $ftime) {
1770
                        // One of the files we knew about previously has changed
1771
                        // so we should look at our incoming root again.
1772
                        $root = $in['root'];
1773
                        break;
1774
                    }
1775
                }
1776
            }
1777
        } else {
1778
            // TODO: Throw an exception? We got neither a string nor something
1779
            // that looks like a compatible lessphp cache structure.
1780
            return null;
1781
        }
1782
1783 1
        if ($root !== null) {
1784
            // If we have a root value which means we should rebuild.
1785 1
            $out = [];
1786 1
            $out['root'] = $root;
1787 1
            $out['compiled'] = $this->compileFile($root);
1788 1
            $out['files'] = $this->allParsedFiles();
1789 1
            $out['updated'] = time();
1790
1791 1
            return $out;
1792
        } else {
1793
            // No changes, pass back the structure
1794
            // we were given initially.
1795
            return $in;
1796
        }
1797
    }
1798
1799
    // parse and compile buffer
1800
    // This is deprecated
1801
    public function parse($str = null, $initialVariables = null)
1802
    {
1803 38
        if (is_array($str)) {
1804 1
            $initialVariables = $str;
1805 1
            $str = null;
1806
        }
1807
1808 38
        $oldVars = $this->registeredVars;
1809 38
        if ($initialVariables !== null) {
1810 2
            $this->setVariables($initialVariables);
1811
        }
1812
1813 38
        if ($str === null) {
1814 1
            if (empty($this->_parseFile)) {
1815
                throw new GeneralException("nothing to parse");
1816
            }
1817
1818 1
            $out = $this->compileFile($this->_parseFile);
1819
        } else {
1820 37
            $out = $this->compile($str);
1821
        }
1822
1823 38
        $this->registeredVars = $oldVars;
1824
1825 38
        return $out;
1826
    }
1827
1828
    protected function makeParser($name)
1829
    {
1830 50
        $parser = new \LesserPhp\Parser($this, $name);
1831 50
        $parser->writeComments = $this->preserveComments;
0 ignored issues
show
Bug introduced by
The property writeComments does not seem to exist in LesserPhp\Parser.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1832
1833 50
        return $parser;
1834
    }
1835
1836
    public function setFormatter($name)
1837
    {
1838 1
        $this->formatterName = $name;
1839 1
    }
1840
1841
    /**
1842
     * @return \LesserPhp\Formatter\FormatterInterface
1843
     */
1844
    protected function newFormatter()
1845
    {
1846 50
        $className = 'Lessjs';
1847 50
        if (!empty($this->formatterName)) {
1848 1
            if (!is_string($this->formatterName)) {
1849
                return $this->formatterName;
1850
            }
1851 1
            $className = $this->formatterName;
1852
        }
1853
1854 50
        $className = '\LesserPhp\Formatter\\' . $className;
1855
1856 50
        return new $className;
1857
    }
1858
1859
    public function setPreserveComments($preserve)
1860
    {
1861 1
        $this->preserveComments = $preserve;
1862 1
    }
1863
1864
    public function registerFunction($name, $func)
1865
    {
1866 1
        $this->libFunctions[$name] = $func;
1867 1
    }
1868
1869
    public function unregisterFunction($name)
1870
    {
1871 1
        unset($this->libFunctions[$name]);
1872 1
    }
1873
1874
    public function setVariables($variables)
1875
    {
1876 2
        $this->registeredVars = array_merge($this->registeredVars, $variables);
1877 2
    }
1878
1879
    public function unsetVariable($name)
1880
    {
1881
        unset($this->registeredVars[$name]);
1882
    }
1883
1884
    public function setImportDir($dirs)
1885
    {
1886 1
        $this->importDir = (array)$dirs;
1887 1
    }
1888
1889
    public function addImportDir($dir)
1890
    {
1891
        $this->importDir = (array)$this->importDir;
1892
        $this->importDir[] = $dir;
1893
    }
1894
1895
    public function allParsedFiles()
1896
    {
1897 1
        return $this->allParsedFiles;
1898
    }
1899
1900
    public function addParsedFile($file)
1901
    {
1902 3
        $this->allParsedFiles[realpath($file)] = filemtime($file);
1903 3
    }
1904
1905
    /**
1906
     * Uses the current value of $this->count to show line and line number
1907
     *
1908
     * @param string $msg
1909
     *
1910
     * @throws \Exception
1911
     */
1912
    public function throwError($msg = null)
1913
    {
1914
        if ($this->sourceLoc >= 0) {
1915
            $this->sourceParser->throwError($msg, $this->sourceLoc);
1916
        }
1917
        throw new GeneralException($msg);
1918
    }
1919
1920
    // compile file $in to file $out if $in is newer than $out
1921
    // returns true when it compiles, false otherwise
1922
    public static function ccompile($in, $out, Compiler $less = null)
1923
    {
1924
        if ($less === null) {
1925
            $less = new self;
1926
        }
1927
1928
        return $less->checkedCompile($in, $out);
1929
    }
1930
1931
    public static function cexecute($in, $force = false, Compiler $less = null)
1932
    {
1933
        if ($less === null) {
1934
            $less = new self;
1935
        }
1936
1937
        return $less->cachedCompile($in, $force);
1938
    }
1939
}
1940