Completed
Push — master ( 14f6f1...9a86cf )
by Marcus
02:46
created

Compiler::equals()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 2
crap 1
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
49
{
50
51
    const VERSION = 'v0.5.1';
52
53
    static public $TRUE = ['keyword', 'true'];
54
    static public $FALSE = ['keyword', 'false'];
55
56
    /**
57
     * @var callable[]
58
     */
59
    private $libFunctions = [];
60
61
    /**
62
     * @var string[]
63
     */
64
    private $registeredVars = [];
65
66
    /**
67
     * @var bool
68
     */
69
    protected $preserveComments = false;
70
71
    /**
72
     * @var string $vPrefix prefix of abstract properties
73
     */
74
    private $vPrefix = '@';
75
76
    /**
77
     * @var string $mPrefix prefix of abstract blocks
78
     */
79
    private $mPrefix = '$';
80
81
    /**
82
     * @var string
83
     */
84
    private $parentSelector = '&';
85
86
    /**
87
     * @var bool $importDisabled disable @import
88
     */
89
    private $importDisabled = false;
90
91
    /**
92
     * @var string[]
93
     */
94
    private $importDirs = [];
95
96
    /**
97
     * @var int
98
     */
99
    private $numberPrecision;
100
101
    /**
102
     * @var string[]
103
     */
104
    private $allParsedFiles = [];
105
106
    /**
107
     * set to the parser that generated the current line when compiling
108
     * so we know how to create error messages
109
     * @var \LesserPhp\Parser
110
     */
111
    private $sourceParser;
112
113
    /**
114
     * @var integer $sourceLoc Lines of Code
115
     */
116
    private $sourceLoc;
117
118
    /**
119
     * @var int $nextImportId uniquely identify imports
120
     */
121
    static private $nextImportId = 0;
122
123
    /**
124
     * @var Parser
125
     */
126
    private $parser;
127
128
    /**
129
     * @var \LesserPhp\Formatter\FormatterInterface
130
     */
131
    private $formatter;
132
133
    /**
134
     * @var \LesserPhp\NodeEnv What's the meaning of "env" in this context?
135
     */
136
    private $env;
137
138
    /**
139
     * @var \LesserPhp\Library\Coerce
140
     */
141
    private $coerce;
142
143
    /**
144
     * @var \LesserPhp\Library\Assertions
145
     */
146
    private $assertions;
147
148
    /**
149
     * @var \LesserPhp\Library\Functions
150
     */
151
    private $functions;
152
153
    /**
154
     * @var mixed what's this exactly?
155
     */
156
    private $scope;
157
158
    /**
159
     * @var string
160
     */
161
    private $formatterName;
162
163
    /**
164
     * @var \LesserPhp\Color\Converter
165
     */
166
    private $converter;
167
168
    /**
169
     * Constructor.
170
     *
171
     * Hardwires dependencies for now
172
     */
173 49
    public function __construct()
174
    {
175 49
        $this->coerce = new Coerce();
176 49
        $this->assertions = new Assertions($this->coerce);
177 49
        $this->converter = new Converter();
178 49
        $this->functions = new Functions($this->assertions, $this->coerce, $this, $this->converter);
179 49
    }
180
181
    /**
182
     * attempts to find the path of an import url, returns null for css files
183
     *
184
     * @param $url
185
     *
186
     * @return null|string
187
     */
188 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...
189
    {
190 3
        foreach ($this->importDirs as $dir) {
191 3
            $full = $dir . (mb_substr($dir, -1) !== '/' ? '/' : '') . $url;
192 3
            if ($this->fileExists($file = $full . '.less') || $this->fileExists($file = $full)) {
193 3
                return $file;
194
            }
195
        }
196
197 2
        return null;
198
    }
199
200
    /**
201
     * @param string $name
202
     *
203
     * @return bool
204
     */
205 3
    protected function fileExists($name)
206
    {
207 3
        return is_file($name);
208
    }
209
210
    /**
211
     * @param array $items
212
     * @param       $delim
213
     *
214
     * @return array
215
     */
216 48
    public static function compressList(array $items, $delim)
217
    {
218 48
        if (!isset($items[1]) && isset($items[0])) {
219 48
            return $items[0];
220
        } else {
221 27
            return ['list', $delim, $items];
222
        }
223
    }
224
225
    /**
226
     * @param string $what
227
     *
228
     * @return string
229
     */
230 36
    public static function pregQuote($what)
231
    {
232 36
        return preg_quote($what, '/');
233
    }
234
235
    /**
236
     * @param array $importPath
237
     * @param       $parentBlock
238
     * @param       $out
239
     *
240
     * @return array|false
241
     * @throws \LesserPhp\Exception\GeneralException
242
     */
243 3
    protected function tryImport(array $importPath, $parentBlock, $out)
244
    {
245 3
        if ($importPath[0] === 'function' && $importPath[1] === 'url') {
246 2
            $importPath = $this->flattenList($importPath[2]);
247
        }
248
249 3
        $str = $this->coerce->coerceString($importPath);
250 3
        if ($str === null) {
251 2
            return false;
252
        }
253
254 3
        $url = $this->compileValue($this->functions->e($str));
255
256
        // don't import if it ends in css
257 3
        if (substr_compare($url, '.css', -4, 4) === 0) {
258
            return false;
259
        }
260
261 3
        $realPath = $this->findImport($url);
262
263 3
        if ($realPath === null) {
264 2
            return false;
265
        }
266
267 3
        if ($this->isImportDisabled()) {
268 1
            return [false, '/* import disabled */'];
269
        }
270
271 2
        if (isset($this->allParsedFiles[realpath($realPath)])) {
272 2
            return [false, null];
273
        }
274
275 2
        $this->addParsedFile($realPath);
276 2
        $parser = $this->makeParser($realPath);
277 2
        $root = $parser->parse(file_get_contents($realPath));
278
279
        // set the parents of all the block props
280 2
        foreach ($root->props as $prop) {
281 2
            if ($prop[0] === 'block') {
282 2
                $prop[1]->parent = $parentBlock;
283
            }
284
        }
285
286
        // copy mixins into scope, set their parents
287
        // bring blocks from import into current block
288
        // TODO: need to mark the source parser	these came from this file
289 2
        foreach ($root->children as $childName => $child) {
290 2
            if (isset($parentBlock->children[$childName])) {
291 2
                $parentBlock->children[$childName] = array_merge(
292 2
                    $parentBlock->children[$childName],
293
                    $child
294
                );
295
            } else {
296 2
                $parentBlock->children[$childName] = $child;
297
            }
298
        }
299
300 2
        $pi = pathinfo($realPath);
301 2
        $dir = $pi["dirname"];
302
303 2
        list($top, $bottom) = $this->sortProps($root->props, true);
304 2
        $this->compileImportedProps($top, $parentBlock, $out, $dir);
305
306 2
        return [true, $bottom, $parser, $dir];
307
    }
308
309
    /**
310
     * @param array  $props
311
     * @param        $block
312
     * @param        $out
313
     * @param string $importDir
314
     */
315 2
    protected function compileImportedProps(array $props, $block, $out, $importDir)
316
    {
317 2
        $oldSourceParser = $this->sourceParser;
318
319 2
        $oldImport = $this->importDirs;
320
321 2
        array_unshift($this->importDirs, $importDir);
322
323 2
        foreach ($props as $prop) {
324 2
            $this->compileProp($prop, $block, $out);
325
        }
326
327 2
        $this->importDirs = $oldImport;
328 2
        $this->sourceParser = $oldSourceParser;
329 2
    }
330
331
    /**
332
     * Recursively compiles a block.
333
     *
334
     * A block is analogous to a CSS block in most cases. A single LESS document
335
     * is encapsulated in a block when parsed, but it does not have parent tags
336
     * so all of it's children appear on the root level when compiled.
337
     *
338
     * Blocks are made up of props and children.
339
     *
340
     * Props are property instructions, array tuples which describe an action
341
     * to be taken, eg. write a property, set a variable, mixin a block.
342
     *
343
     * The children of a block are just all the blocks that are defined within.
344
     * This is used to look up mixins when performing a mixin.
345
     *
346
     * Compiling the block involves pushing a fresh environment on the stack,
347
     * and iterating through the props, compiling each one.
348
     *
349
     * See lessc::compileProp()
350
     *
351
     * @param $block
352
     */
353 49
    protected function compileBlock($block)
354
    {
355 49
        switch ($block->type) {
356 49
            case "root":
357 49
                $this->compileRoot($block);
358 38
                break;
359 46
            case null:
360 46
                $this->compileCSSBlock($block);
361 35
                break;
362 6
            case "media":
363 3
                $this->compileMedia($block);
364 3
                break;
365 4
            case "directive":
366 4
                $name = "@" . $block->name;
367 4
                if (!empty($block->value)) {
368 2
                    $name .= " " . $this->compileValue($this->reduce($block->value));
369
                }
370
371 4
                $this->compileNestedBlock($block, [$name]);
372 4
                break;
373
            default:
374
                $block->parser->throwError("unknown block type: $block->type\n", $block->count);
375
        }
376 38
    }
377
378
    /**
379
     * @param $block
380
     */
381 46
    protected function compileCSSBlock($block)
382
    {
383 46
        $env = $this->pushEnv($this->env);
384
385 46
        $selectors = $this->compileSelectors($block->tags);
386 46
        $env->setSelectors($this->multiplySelectors($selectors));
387 46
        $out = $this->makeOutputBlock(null, $env->getSelectors());
0 ignored issues
show
Bug introduced by
It seems like $env->getSelectors() targeting LesserPhp\NodeEnv::getSelectors() can also be of type array; however, LesserPhp\Compiler::makeOutputBlock() does only seem to accept null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
388
389 46
        $this->scope->children[] = $out;
390 46
        $this->compileProps($block, $out);
391
392 35
        $block->scope = $env; // mixins carry scope with them!
393 35
        $this->popEnv();
394 35
    }
395
396
    /**
397
     * @param $media
398
     */
399 3
    protected function compileMedia($media)
400
    {
401 3
        $env = $this->pushEnv($this->env, $media);
402 3
        $parentScope = $this->mediaParent($this->scope);
403
404 3
        $query = $this->compileMediaQuery($this->multiplyMedia($env));
0 ignored issues
show
Bug introduced by
It seems like $this->multiplyMedia($env) targeting LesserPhp\Compiler::multiplyMedia() can also be of type null; however, LesserPhp\Compiler::compileMediaQuery() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
405
406 3
        $this->scope = $this->makeOutputBlock($media->type, [$query]);
407 3
        $parentScope->children[] = $this->scope;
408
409 3
        $this->compileProps($media, $this->scope);
410
411 3
        if (count($this->scope->lines) > 0) {
412 3
            $orphanSelelectors = $this->findClosestSelectors();
413 3
            if ($orphanSelelectors !== null) {
414 3
                $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
415 3
                $orphan->lines = $this->scope->lines;
416 3
                array_unshift($this->scope->children, $orphan);
417 3
                $this->scope->lines = [];
418
            }
419
        }
420
421 3
        $this->scope = $this->scope->parent;
422 3
        $this->popEnv();
423 3
    }
424
425
    /**
426
     * @param $scope
427
     *
428
     * @return mixed
429
     */
430 3
    protected function mediaParent($scope)
431
    {
432 3
        while (!empty($scope->parent)) {
433 1
            if (!empty($scope->type) && $scope->type !== "media") {
434 1
                break;
435
            }
436 1
            $scope = $scope->parent;
437
        }
438
439 3
        return $scope;
440
    }
441
442
    /**
443
     * @param $block
444
     * @param $selectors
445
     */
446 4
    protected function compileNestedBlock($block, $selectors)
447
    {
448 4
        $this->pushEnv($this->env, $block);
449 4
        $this->scope = $this->makeOutputBlock($block->type, $selectors);
450 4
        $this->scope->parent->children[] = $this->scope;
451
452 4
        $this->compileProps($block, $this->scope);
453
454 4
        $this->scope = $this->scope->parent;
455 4
        $this->popEnv();
456 4
    }
457
458
    /**
459
     * @param $root
460
     */
461 49
    protected function compileRoot($root)
462
    {
463 49
        $this->pushEnv($this->env);
464 49
        $this->scope = $this->makeOutputBlock($root->type);
465 49
        $this->compileProps($root, $this->scope);
466 38
        $this->popEnv();
467 38
    }
468
469
    /**
470
     * @param $block
471
     * @param $out
472
     */
473 49
    protected function compileProps($block, $out)
474
    {
475 49
        foreach ($this->sortProps($block->props) as $prop) {
476 49
            $this->compileProp($prop, $block, $out);
477
        }
478 38
        $out->lines = $this->deduplicate($out->lines);
479 38
    }
480
481
    /**
482
     * Deduplicate lines in a block. Comments are not deduplicated. If a
483
     * duplicate rule is detected, the comments immediately preceding each
484
     * occurence are consolidated.
485
     *
486
     * @param array $lines
487
     *
488
     * @return array
489
     */
490 38
    protected function deduplicate(array $lines)
491
    {
492 38
        $unique = [];
493 38
        $comments = [];
494
495 38
        foreach ($lines as $line) {
496 38
            if (strpos($line, '/*') === 0) {
497 2
                $comments[] = $line;
498 2
                continue;
499
            }
500 37
            if (!in_array($line, $unique)) {
501 37
                $unique[] = $line;
502
            }
503 37
            array_splice($unique, array_search($line, $unique), 0, $comments);
504 37
            $comments = [];
505
        }
506
507 38
        return array_merge($unique, $comments);
508
    }
509
510
    /**
511
     * @param array $props
512
     * @param bool  $split
513
     *
514
     * @return array
515
     */
516 49
    protected function sortProps(array $props, $split = false)
517
    {
518 49
        $vars = [];
519 49
        $imports = [];
520 49
        $other = [];
521 49
        $stack = [];
522
523 49
        foreach ($props as $prop) {
524 49
            switch ($prop[0]) {
525 49
                case "comment":
526 1
                    $stack[] = $prop;
527 1
                    break;
528 49
                case "assign":
529 43
                    $stack[] = $prop;
530 43
                    if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
531 23
                        $vars = array_merge($vars, $stack);
532
                    } else {
533 43
                        $other = array_merge($other, $stack);
534
                    }
535 43
                    $stack = [];
536 43
                    break;
537 47
                case "import":
538 3
                    $id = self::$nextImportId++;
539 3
                    $prop[] = $id;
540 3
                    $stack[] = $prop;
541 3
                    $imports = array_merge($imports, $stack);
542 3
                    $other[] = ["import_mixin", $id];
543 3
                    $stack = [];
544 3
                    break;
545
                default:
546 46
                    $stack[] = $prop;
547 46
                    $other = array_merge($other, $stack);
548 46
                    $stack = [];
549 49
                    break;
550
            }
551
        }
552 49
        $other = array_merge($other, $stack);
553
554 49
        if ($split) {
555 2
            return [array_merge($imports, $vars), $other];
556
        } else {
557 49
            return array_merge($imports, $vars, $other);
558
        }
559
    }
560
561
    /**
562
     * @param array $queries
563
     *
564
     * @return string
565
     */
566 3
    protected function compileMediaQuery(array $queries)
567
    {
568 3
        $compiledQueries = [];
569 3
        foreach ($queries as $query) {
570 3
            $parts = [];
571 3
            foreach ($query as $q) {
572 3
                switch ($q[0]) {
573 3
                    case "mediaType":
574 3
                        $parts[] = implode(" ", array_slice($q, 1));
575 3
                        break;
576 1
                    case "mediaExp":
577 1
                        if (isset($q[2])) {
578 1
                            $parts[] = "($q[1]: " .
579 1
                                $this->compileValue($this->reduce($q[2])) . ")";
580
                        } else {
581 1
                            $parts[] = "($q[1])";
582
                        }
583 1
                        break;
584 1
                    case "variable":
585 1
                        $parts[] = $this->compileValue($this->reduce($q));
586 3
                        break;
587
                }
588
            }
589
590 3
            if (count($parts) > 0) {
591 3
                $compiledQueries[] = implode(" and ", $parts);
592
            }
593
        }
594
595 3
        $out = "@media";
596 3
        if (!empty($parts)) {
597
            $out .= " " .
598 3
                implode($this->formatter->getSelectorSeparator(), $compiledQueries);
599
        }
600
601 3
        return $out;
602
    }
603
604
    /**
605
     * @param \LesserPhp\NodeEnv $env
606
     * @param array              $childQueries
607
     *
608
     * @return array
609
     */
610 3
    protected function multiplyMedia(NodeEnv $env = null, array $childQueries = null)
611
    {
612 3
        if ($env === null ||
613 3
            (!empty($env->getBlock()->type) && $env->getBlock()->type !== 'media')
614
        ) {
615 3
            return $childQueries;
616
        }
617
618
        // plain old block, skip
619 3
        if (empty($env->getBlock()->type)) {
620 3
            return $this->multiplyMedia($env->getParent(), $childQueries);
621
        }
622
623 3
        $out = [];
624 3
        $queries = $env->getBlock()->queries;
625 3
        if ($childQueries === null) {
626 3
            $out = $queries;
627
        } else {
628 1
            foreach ($queries as $parent) {
629 1
                foreach ($childQueries as $child) {
630 1
                    $out[] = array_merge($parent, $child);
631
                }
632
            }
633
        }
634
635 3
        return $this->multiplyMedia($env->getParent(), $out);
636
    }
637
638
    /**
639
     * @param $tag
640
     * @param $replace
641
     *
642
     * @return int
643
     */
644 46
    protected function expandParentSelectors(&$tag, $replace)
645
    {
646 46
        $parts = explode("$&$", $tag);
647 46
        $count = 0;
648 46
        foreach ($parts as &$part) {
649 46
            $part = str_replace($this->parentSelector, $replace, $part, $c);
650 46
            $count += $c;
651
        }
652 46
        $tag = implode($this->parentSelector, $parts);
653
654 46
        return $count;
655
    }
656
657
    /**
658
     * @return array|null
659
     */
660 46
    protected function findClosestSelectors()
661
    {
662 46
        $env = $this->env;
663 46
        $selectors = null;
664 46
        while ($env !== null) {
665 46
            if ($env->getSelectors() !== null) {
666 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...
667 13
                break;
668
            }
669 46
            $env = $env->getParent();
670
        }
671
672 46
        return $selectors;
673
    }
674
675
676
    /**
677
     *  multiply $selectors against the nearest selectors in env
678
     *
679
     * @param array $selectors
680
     *
681
     * @return array
682
     */
683 46
    protected function multiplySelectors(array $selectors)
684
    {
685
        // find parent selectors
686
687 46
        $parentSelectors = $this->findClosestSelectors();
688 46
        if ($parentSelectors === null) {
689
            // kill parent reference in top level selector
690 46
            foreach ($selectors as &$s) {
691 46
                $this->expandParentSelectors($s, "");
692
            }
693
694 46
            return $selectors;
695
        }
696
697 13
        $out = [];
698 13
        foreach ($parentSelectors as $parent) {
699 13
            foreach ($selectors as $child) {
700 13
                $count = $this->expandParentSelectors($child, $parent);
701
702
                // don't prepend the parent tag if & was used
703 13
                if ($count > 0) {
704 4
                    $out[] = trim($child);
705
                } else {
706 13
                    $out[] = trim($parent . ' ' . $child);
707
                }
708
            }
709
        }
710
711 13
        return $out;
712
    }
713
714
    /**
715
     * reduces selector expressions
716
     *
717
     * @param array $selectors
718
     *
719
     * @return array
720
     * @throws \LesserPhp\Exception\GeneralException
721
     */
722 46
    protected function compileSelectors(array $selectors)
723
    {
724 46
        $out = [];
725
726 46
        foreach ($selectors as $s) {
727 46
            if (is_array($s)) {
728 4
                list(, $value) = $s;
729 4
                $out[] = trim($this->compileValue($this->reduce($value)));
730
            } else {
731 46
                $out[] = $s;
732
            }
733
        }
734
735 46
        return $out;
736
    }
737
738
    /**
739
     * @param $left
740
     * @param $right
741
     *
742
     * @return bool
743
     */
744 4
    protected function equals($left, $right)
745
    {
746 4
        return $left == $right;
747
    }
748
749
    /**
750
     * @param $block
751
     * @param $orderedArgs
752
     * @param $keywordArgs
753
     *
754
     * @return bool
755
     */
756 21
    protected function patternMatch($block, $orderedArgs, $keywordArgs)
757
    {
758
        // match the guards if it has them
759
        // any one of the groups must have all its guards pass for a match
760 21
        if (!empty($block->guards)) {
761 5
            $groupPassed = false;
762 5
            foreach ($block->guards as $guardGroup) {
763 5
                foreach ($guardGroup as $guard) {
764 5
                    $this->pushEnv($this->env);
765 5
                    $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
766
767 5
                    $negate = false;
768 5
                    if ($guard[0] === "negate") {
769 1
                        $guard = $guard[1];
770 1
                        $negate = true;
771
                    }
772
773 5
                    $passed = $this->reduce($guard) == self::$TRUE;
774 5
                    if ($negate) {
775 1
                        $passed = !$passed;
776
                    }
777
778 5
                    $this->popEnv();
779
780 5
                    if ($passed) {
781 3
                        $groupPassed = true;
782
                    } else {
783 5
                        $groupPassed = false;
784 5
                        break;
785
                    }
786
                }
787
788 5
                if ($groupPassed) {
789 5
                    break;
790
                }
791
            }
792
793 5
            if (!$groupPassed) {
794 5
                return false;
795
            }
796
        }
797
798 19
        if (empty($block->args)) {
799 12
            return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
800
        }
801
802 14
        $remainingArgs = $block->args;
803 14
        if ($keywordArgs) {
804 2
            $remainingArgs = [];
805 2
            foreach ($block->args as $arg) {
806 2
                if ($arg[0] === "arg" && isset($keywordArgs[$arg[1]])) {
807 2
                    continue;
808
                }
809
810 2
                $remainingArgs[] = $arg;
811
            }
812
        }
813
814 14
        $i = -1; // no args
815
        // try to match by arity or by argument literal
816 14
        foreach ($remainingArgs as $i => $arg) {
817 14
            switch ($arg[0]) {
818 14
                case "lit":
819 3
                    if (empty($orderedArgs[$i]) || !$this->equals($arg[1], $orderedArgs[$i])) {
820 2
                        return false;
821
                    }
822 3
                    break;
823 14
                case "arg":
824
                    // no arg and no default value
825 14
                    if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
826 3
                        return false;
827
                    }
828 14
                    break;
829 2
                case "rest":
830 2
                    $i--; // rest can be empty
831 14
                    break 2;
832
            }
833
        }
834
835 13
        if ($block->isVararg) {
836 2
            return true; // not having enough is handled above
837
        } else {
838 13
            $numMatched = $i + 1;
839
840
            // greater than because default values always match
841 13
            return $numMatched >= count($orderedArgs);
842
        }
843
    }
844
845
    /**
846
     * @param array $blocks
847
     * @param       $orderedArgs
848
     * @param       $keywordArgs
849
     * @param array $skip
850
     *
851
     * @return array|null
852
     */
853 21
    protected function patternMatchAll(array $blocks, $orderedArgs, $keywordArgs, array $skip = [])
854
    {
855 21
        $matches = null;
856 21
        foreach ($blocks as $block) {
857
            // skip seen blocks that don't have arguments
858 21
            if (isset($skip[$block->id]) && !isset($block->args)) {
859 1
                continue;
860
            }
861
862 21
            if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
863 21
                $matches[] = $block;
864
            }
865
        }
866
867 21
        return $matches;
868
    }
869
870
    /**
871
     * attempt to find blocks matched by path and args
872
     *
873
     * @param       $searchIn
874
     * @param array $path
875
     * @param       $orderedArgs
876
     * @param       $keywordArgs
877
     * @param array $seen
878
     *
879
     * @return array|null
880
     */
881 22
    protected function findBlocks($searchIn, array $path, $orderedArgs, $keywordArgs, array $seen = [])
882
    {
883 22
        if ($searchIn === null) {
884 5
            return null;
885
        }
886 22
        if (isset($seen[$searchIn->id])) {
887 1
            return null;
888
        }
889 22
        $seen[$searchIn->id] = true;
890
891 22
        $name = $path[0];
892
893 22
        if (isset($searchIn->children[$name])) {
894 21
            $blocks = $searchIn->children[$name];
895 21
            if (count($path) === 1) {
896 21
                $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
897 21
                if (!empty($matches)) {
898
                    // This will return all blocks that match in the closest
899
                    // scope that has any matching block, like lessjs
900 21
                    return $matches;
901
                }
902
            } else {
903 3
                $matches = [];
904 3
                foreach ($blocks as $subBlock) {
905 3
                    $subMatches = $this->findBlocks(
906
                        $subBlock,
907 3
                        array_slice($path, 1),
908
                        $orderedArgs,
909
                        $keywordArgs,
910
                        $seen
911
                    );
912
913 3
                    if ($subMatches !== null) {
914 3
                        foreach ($subMatches as $sm) {
915 3
                            $matches[] = $sm;
916
                        }
917
                    }
918
                }
919
920 3
                return count($matches) > 0 ? $matches : null;
921
            }
922
        }
923 22
        if ($searchIn->parent === $searchIn) {
924
            return null;
925
        }
926
927 22
        return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
928
    }
929
930
    /**
931
     * sets all argument names in $args to either the default value
932
     * or the one passed in through $values
933
     *
934
     * @param array $args
935
     * @param       $orderedValues
936
     * @param       $keywordValues
937
     *
938
     * @throws \LesserPhp\Exception\GeneralException
939
     */
940 16
    protected function zipSetArgs(array $args, $orderedValues, $keywordValues)
941
    {
942 16
        $assignedValues = [];
943
944 16
        $i = 0;
945 16
        foreach ($args as $a) {
946 14
            if ($a[0] === "arg") {
947 14
                if (isset($keywordValues[$a[1]])) {
948
                    // has keyword arg
949 2
                    $value = $keywordValues[$a[1]];
950 14
                } elseif (isset($orderedValues[$i])) {
951
                    // has ordered arg
952 13
                    $value = $orderedValues[$i];
953 13
                    $i++;
954 6
                } elseif (isset($a[2])) {
955
                    // has default value
956 6
                    $value = $a[2];
957
                } else {
958
                    throw new GeneralException('Failed to assign arg ' . $a[1]);
959
                }
960
961 14
                $value = $this->reduce($value);
962 14
                $this->set($a[1], $value);
963 14
                $assignedValues[] = $value;
964
            } else {
965
                // a lit
966 14
                $i++;
967
            }
968
        }
969
970
        // check for a rest
971 16
        $last = end($args);
972 16
        if ($last[0] === "rest") {
973 2
            $rest = array_slice($orderedValues, count($args) - 1);
974 2
            $this->set($last[1], $this->reduce(["list", " ", $rest]));
975
        }
976
977
        // wow is this the only true use of PHP's + operator for arrays?
978 16
        $this->env->setArguments($assignedValues + $orderedValues);
979 16
    }
980
981
    /**
982
     * compile a prop and update $lines or $blocks appropriately
983
     *
984
     * @param $prop
985
     * @param $block
986
     * @param $out
987
     *
988
     * @throws \LesserPhp\Exception\GeneralException
989
     */
990 49
    protected function compileProp($prop, $block, $out)
991
    {
992
        // set error position context
993 49
        $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
994
995 49
        switch ($prop[0]) {
996 49
            case 'assign':
997 43
                list(, $name, $value) = $prop;
998 43
                if ($name[0] == $this->vPrefix) {
999 23
                    $this->set($name, $value);
1000
                } else {
1001 43
                    $out->lines[] = $this->formatter->property(
1002
                        $name,
1003 43
                        $this->compileValue($this->reduce($value))
1004
                    );
1005
                }
1006 37
                break;
1007 47
            case 'block':
1008 46
                list(, $child) = $prop;
1009 46
                $this->compileBlock($child);
1010 35
                break;
1011 25
            case 'ruleset':
1012 25
            case 'mixin':
1013 22
                list(, $path, $args, $suffix) = $prop;
1014
1015 22
                $orderedArgs = [];
1016 22
                $keywordArgs = [];
1017 22
                foreach ((array)$args as $arg) {
1018 15
                    switch ($arg[0]) {
1019 15
                        case "arg":
1020 4
                            if (!isset($arg[2])) {
1021 3
                                $orderedArgs[] = $this->reduce(["variable", $arg[1]]);
1022
                            } else {
1023 2
                                $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
1024
                            }
1025 4
                            break;
1026
1027 15
                        case "lit":
1028 15
                            $orderedArgs[] = $this->reduce($arg[1]);
1029 15
                            break;
1030
                        default:
1031 15
                            throw new GeneralException("Unknown arg type: " . $arg[0]);
1032
                    }
1033
                }
1034
1035 22
                $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
1036
1037 22
                if ($mixins === null) {
1038 5
                    $block->parser->throwError("{$prop[1][0]} is undefined", $block->count);
1039
                }
1040
1041 17
                if (strpos($prop[1][0], "$") === 0) {
1042
                    //Use Ruleset Logic - Only last element
1043 8
                    $mixins = [array_pop($mixins)];
1044
                }
1045
1046 17
                foreach ($mixins as $mixin) {
0 ignored issues
show
Bug introduced by
The expression $mixins of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1047 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...
1048
                        continue;
1049
                    }
1050
1051 17
                    $haveScope = false;
1052 17
                    if (isset($mixin->parent->scope)) {
1053 2
                        $haveScope = true;
1054 2
                        $mixinParentEnv = $this->pushEnv($this->env);
1055 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...
1056
                    }
1057
1058 17
                    $haveArgs = false;
1059 17
                    if (isset($mixin->args)) {
1060 14
                        $haveArgs = true;
1061 14
                        $this->pushEnv($this->env);
1062 14
                        $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
1063
                    }
1064
1065 17
                    $oldParent = $mixin->parent;
1066 17
                    if ($mixin != $block) {
1067 17
                        $mixin->parent = $block;
1068
                    }
1069
1070 17
                    foreach ($this->sortProps($mixin->props) as $subProp) {
1071 17
                        if ($suffix !== null &&
1072 17
                            $subProp[0] === "assign" &&
1073 17
                            is_string($subProp[1]) &&
1074 17
                            $subProp[1]{0} != $this->vPrefix
1075
                        ) {
1076 1
                            $subProp[2] = [
1077 1
                                'list',
1078 1
                                ' ',
1079 1
                                [$subProp[2], ['keyword', $suffix]],
1080
                            ];
1081
                        }
1082
1083 17
                        $this->compileProp($subProp, $mixin, $out);
1084
                    }
1085
1086 17
                    $mixin->parent = $oldParent;
1087
1088 17
                    if ($haveArgs) {
1089 14
                        $this->popEnv();
1090
                    }
1091 17
                    if ($haveScope) {
1092 17
                        $this->popEnv();
1093
                    }
1094
                }
1095
1096 17
                break;
1097 5
            case 'raw':
1098
                $out->lines[] = $prop[1];
1099
                break;
1100 5
            case "directive":
1101 1
                list(, $name, $value) = $prop;
1102 1
                $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)) . ';';
1103 1
                break;
1104 4
            case "comment":
1105 1
                $out->lines[] = $prop[1];
1106 1
                break;
1107 3
            case "import":
1108 3
                list(, $importPath, $importId) = $prop;
1109 3
                $importPath = $this->reduce($importPath);
1110
1111 3
                $result = $this->tryImport($importPath, $block, $out);
1112
1113 3
                $this->env->addImports($importId, $result === false ?
1114 2
                    [false, "@import " . $this->compileValue($importPath) . ";"] :
1115 3
                    $result);
1116
1117 3
                break;
1118 3
            case "import_mixin":
1119 3
                list(, $importId) = $prop;
1120 3
                $import = $this->env->getImports($importId);
1121 3
                if ($import[0] === false) {
1122 3
                    if (isset($import[1])) {
1123 3
                        $out->lines[] = $import[1];
1124
                    }
1125
                } else {
1126 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...
1127 2
                    $this->compileImportedProps($bottom, $block, $out, $importDir);
1128
                }
1129
1130 3
                break;
1131
            default:
1132
                $block->parser->throwError("unknown op: {$prop[0]}\n", $block->count);
1133
        }
1134 38
    }
1135
1136
1137
    /**
1138
     * Compiles a primitive value into a CSS property value.
1139
     *
1140
     * Values in lessphp are typed by being wrapped in arrays, their format is
1141
     * typically:
1142
     *
1143
     *     array(type, contents [, additional_contents]*)
1144
     *
1145
     * The input is expected to be reduced. This function will not work on
1146
     * things like expressions and variables.
1147
     *
1148
     * @param array $value
1149
     *
1150
     * @return string
1151
     * @throws \LesserPhp\Exception\GeneralException
1152
     */
1153 38
    public function compileValue(array $value)
1154
    {
1155 38
        switch ($value[0]) {
1156 38
            case 'list':
1157
                // [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...
1158
                // [2] - array of values
1159 21
                return implode($value[1], array_map([$this, 'compileValue'], $value[2]));
1160 38
            case 'raw_color':
1161 8
                if ($this->formatter->getCompressColors()) {
1162
                    return $this->compileValue($this->coerce->coerceColor($value));
1163
                }
1164
1165 8
                return $value[1];
1166 38
            case 'keyword':
1167
                // [1] - the keyword
1168 32
                return $value[1];
1169 36
            case 'number':
1170 32
                list(, $num, $unit) = $value;
1171
                // [1] - the number
1172
                // [2] - the unit
1173 32
                if ($this->numberPrecision !== null) {
1174
                    $num = round($num, $this->numberPrecision);
1175
                }
1176
1177 32
                return $num . $unit;
1178 29
            case 'string':
1179
                // [1] - contents of string (includes quotes)
1180 24
                list(, $delim, $content) = $value;
1181 24
                foreach ($content as &$part) {
1182 24
                    if (is_array($part)) {
1183 24
                        $part = $this->compileValue($part);
1184
                    }
1185
                }
1186
1187 24
                return $delim . implode($content) . $delim;
1188 17
            case 'color':
1189
                // [1] - red component (either number or a %)
1190
                // [2] - green component
1191
                // [3] - blue component
1192
                // [4] - optional alpha component
1193 8
                list(, $r, $g, $b) = $value;
1194 8
                $r = round($r);
1195 8
                $g = round($g);
1196 8
                $b = round($b);
1197
1198 8
                if (count($value) === 5 && $value[4] != 1) { // rgba
1199 3
                    return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $value[4] . ')';
1200
                }
1201
1202 8
                $h = sprintf("#%02x%02x%02x", $r, $g, $b);
1203
1204 8
                if ($this->formatter->getCompressColors()) {
1205
                    // Converting hex color to short notation (e.g. #003399 to #039)
1206 1
                    if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
1207 1
                        $h = '#' . $h[1] . $h[3] . $h[5];
1208
                    }
1209
                }
1210
1211 8
                return $h;
1212
1213 10
            case 'function':
1214 10
                list(, $name, $args) = $value;
1215
1216 10
                return $name . '(' . $this->compileValue($args) . ')';
1217
            default: // assumed to be unit
1218
                throw new GeneralException('unknown value type: ' . $value[0]);
1219
        }
1220
    }
1221
1222
    /**
1223
     * Helper function to get arguments for color manipulation functions.
1224
     * takes a list that contains a color like thing and a percentage
1225
     *
1226
     * @param array $args
1227
     *
1228
     * @return array
1229
     */
1230 2
    public function colorArgs(array $args)
1231
    {
1232 2
        if ($args[0] !== 'list' || count($args[2]) < 2) {
1233 1
            return [['color', 0, 0, 0], 0];
1234
        }
1235 2
        list($color, $delta) = $args[2];
1236 2
        $color = $this->assertions->assertColor($color);
1237 2
        $delta = (float)$delta[1];
1238
1239 2
        return [$color, $delta];
1240
    }
1241
1242
    /**
1243
     * Convert the rgb, rgba, hsl color literals of function type
1244
     * as returned by the parser into values of color type.
1245
     *
1246
     * @param array $func
1247
     *
1248
     * @return bool|mixed
1249
     */
1250 24
    protected function funcToColor(array $func)
1251
    {
1252 24
        $fname = $func[1];
1253 24
        if ($func[2][0] !== 'list') {
1254 6
            return false;
1255
        } // need a list of arguments
1256
        /** @var array $rawComponents */
1257 24
        $rawComponents = $func[2][2];
1258
1259 24
        if ($fname === 'hsl' || $fname === 'hsla') {
1260 1
            $hsl = ['hsl'];
1261 1
            $i = 0;
1262 1
            foreach ($rawComponents as $c) {
1263 1
                $val = $this->reduce($c);
1264 1
                $val = isset($val[1]) ? (float)$val[1] : 0;
1265
1266 1
                if ($i === 0) {
1267 1
                    $clamp = 360;
1268 1
                } elseif ($i < 3) {
1269 1
                    $clamp = 100;
1270
                } else {
1271 1
                    $clamp = 1;
1272
                }
1273
1274 1
                $hsl[] = $this->converter->clamp($val, $clamp);
1275 1
                $i++;
1276
            }
1277
1278 1
            while (count($hsl) < 4) {
1279
                $hsl[] = 0;
1280
            }
1281
1282 1
            return $this->converter->toRGB($hsl);
1283
1284 24
        } elseif ($fname === 'rgb' || $fname === 'rgba') {
1285 4
            $components = [];
1286 4
            $i = 1;
1287 4
            foreach ($rawComponents as $c) {
1288 4
                $c = $this->reduce($c);
1289 4
                if ($i < 4) {
1290 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...
1291 1
                        $components[] = 255 * ($c[1] / 100);
1292
                    } else {
1293 4
                        $components[] = (float)$c[1];
1294
                    }
1295 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...
1296 4
                    if ($c[0] === "number" && $c[2] === "%") {
1297
                        $components[] = 1.0 * ($c[1] / 100);
1298
                    } else {
1299 4
                        $components[] = (float)$c[1];
1300
                    }
1301
                } else {
1302
                    break;
1303
                }
1304
1305 4
                $i++;
1306
            }
1307 4
            while (count($components) < 3) {
1308
                $components[] = 0;
1309
            }
1310 4
            array_unshift($components, 'color');
1311
1312 4
            return $this->fixColor($components);
1313
        }
1314
1315 23
        return false;
1316
    }
1317
1318
    /**
1319
     * @param array $value
1320
     * @param bool  $forExpression
1321
     *
1322
     * @return array|bool|mixed|null // <!-- dafuq?
1323
     */
1324 48
    public function reduce(array $value, $forExpression = false)
1325
    {
1326 48
        switch ($value[0]) {
1327 48
            case "interpolate":
1328 7
                $reduced = $this->reduce($value[1]);
1329 7
                $var = $this->compileValue($reduced);
1330 7
                $res = $this->reduce(["variable", $this->vPrefix . $var]);
1331
1332 7
                if ($res[0] === "raw_color") {
1333 1
                    $res = $this->coerce->coerceColor($res);
1334
                }
1335
1336 7
                if (empty($value[2])) {
1337 6
                    $res = $this->functions->e($res);
1338
                }
1339
1340 7
                return $res;
1341 48
            case "variable":
1342 28
                $key = $value[1];
1343 28
                if (is_array($key)) {
1344 1
                    $key = $this->reduce($key);
1345 1
                    $key = $this->vPrefix . $this->compileValue($this->functions->e($key));
1346
                }
1347
1348 28
                $seen =& $this->env->seenNames;
1349
1350 28
                if (!empty($seen[$key])) {
1351
                    $this->throwError("infinite loop detected: $key");
1352
                }
1353
1354 28
                $seen[$key] = true;
1355 28
                $out = $this->reduce($this->get($key));
1356 27
                $seen[$key] = false;
1357
1358 27
                return $out;
1359 47
            case "list":
1360 29
                foreach ($value[2] as &$item) {
1361 26
                    $item = $this->reduce($item, $forExpression);
1362
                }
1363
1364 29
                return $value;
1365 47
            case "expression":
1366 19
                return $this->evaluate($value);
1367 47
            case "string":
1368 26
                foreach ($value[2] as &$part) {
1369 26
                    if (is_array($part)) {
1370 11
                        $strip = $part[0] === "variable";
1371 11
                        $part = $this->reduce($part);
1372 11
                        if ($strip) {
1373 26
                            $part = $this->functions->e($part);
1374
                        }
1375
                    }
1376
                }
1377
1378 26
                return $value;
1379 45
            case "escape":
1380 4
                list(, $inner) = $value;
1381
1382 4
                return $this->functions->e($this->reduce($inner));
1383 45
            case "function":
1384 24
                $color = $this->funcToColor($value);
1385 24
                if ($color) {
1386 4
                    return $color;
1387
                }
1388
1389 23
                list(, $name, $args) = $value;
1390 23
                if ($name === "%") {
1391 1
                    $name = "_sprintf";
1392
                }
1393
1394
                // user functions
1395 23
                $f = null;
1396 23
                if (isset($this->libFunctions[$name]) && is_callable($this->libFunctions[$name])) {
1397 1
                    $f = $this->libFunctions[$name];
1398
                }
1399
1400 23
                $func = str_replace('-', '_', $name);
1401
1402 23
                if ($f !== null || method_exists($this->functions, $func)) {
1403 14
                    if ($args[0] === 'list') {
1404 14
                        $args = self::compressList($args[2], $args[1]);
1405
                    }
1406
1407 14
                    if ($f !== null) {
1408 1
                        $ret = $f($this->reduce($args, true), $this);
1409
                    } else {
1410 13
                        $ret = $this->functions->$func($this->reduce($args, true), $this);
1411
                    }
1412 9
                    if ($ret === null) {
1413
                        return [
1414 2
                            "string",
1415 2
                            "",
1416
                            [
1417 2
                                $name,
1418 2
                                "(",
1419 2
                                $args,
1420 2
                                ")",
1421
                            ],
1422
                        ];
1423
                    }
1424
1425
                    // convert to a typed value if the result is a php primitive
1426 8
                    if (is_numeric($ret)) {
1427 3
                        $ret = ['number', $ret, ""];
1428 7
                    } elseif (!is_array($ret)) {
1429 2
                        $ret = ['keyword', $ret];
1430
                    }
1431
1432 8
                    return $ret;
1433
                }
1434
1435
                // plain function, reduce args
1436 10
                $value[2] = $this->reduce($value[2]);
1437
1438 10
                return $value;
1439 40
            case "unary":
1440 6
                list(, $op, $exp) = $value;
1441 6
                $exp = $this->reduce($exp);
1442
1443 6
                if ($exp[0] === "number") {
1444
                    switch ($op) {
1445 6
                        case "+":
1446
                            return $exp;
1447 6
                        case "-":
1448 6
                            $exp[1] *= -1;
1449
1450 6
                            return $exp;
1451
                    }
1452
                }
1453
1454
                return ["string", "", [$op, $exp]];
1455
        }
1456
1457 40
        if ($forExpression) {
1458 22
            switch ($value[0]) {
1459 22
                case "keyword":
1460 5
                    $color = $this->coerce->coerceColor($value);
1461 5
                    if ($color !== null) {
1462 2
                        return $color;
1463
                    }
1464 5
                    break;
1465 22
                case "raw_color":
1466 6
                    return $this->coerce->coerceColor($value);
1467
            }
1468
        }
1469
1470 40
        return $value;
1471
    }
1472
1473
    /**
1474
     * turn list of length 1 into value type
1475
     *
1476
     * @param array $value
1477
     *
1478
     * @return array
1479
     */
1480 2
    protected function flattenList(array $value)
1481
    {
1482 2
        if ($value[0] === 'list' && count($value[2]) === 1) {
1483 2
            return $this->flattenList($value[2][0]);
1484
        }
1485
1486 2
        return $value;
1487
    }
1488
1489
    /**
1490
     * @param $a
1491
     *
1492
     * @return array
1493
     */
1494 5
    public function toBool($a)
1495
    {
1496 5
        if ($a) {
1497 4
            return self::$TRUE;
1498
        } else {
1499 5
            return self::$FALSE;
1500
        }
1501
    }
1502
1503
    /**
1504
     * evaluate an expression
1505
     *
1506
     * @param array $exp
1507
     *
1508
     * @return array
1509
     */
1510 19
    protected function evaluate($exp)
1511
    {
1512 19
        list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
1513
1514 19
        $left = $this->reduce($left, true);
1515 19
        $right = $this->reduce($right, true);
1516
1517 19
        $leftColor = $this->coerce->coerceColor($left);
1518 19
        if ($leftColor !== null) {
1519 5
            $left = $leftColor;
1520
        }
1521
1522 19
        $rightColor = $this->coerce->coerceColor($right);
1523 19
        if ($rightColor !== null) {
1524 5
            $right = $rightColor;
1525
        }
1526
1527 19
        $ltype = $left[0];
1528 19
        $rtype = $right[0];
1529
1530
        // operators that work on all types
1531 19
        if ($op === "and") {
1532
            return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
1533
        }
1534
1535 19
        if ($op === "=") {
1536 1
            return $this->toBool($this->equals($left, $right));
1537
        }
1538
1539 19
        $str = $this->stringConcatenate($left, $right);
1540 19
        if ($op === "+" && $str !== null) {
1541 2
            return $str;
1542
        }
1543
1544
        // type based operators
1545 17
        $fname = "op_${ltype}_${rtype}";
1546 17
        if (is_callable([$this, $fname])) {
1547 17
            $out = $this->$fname($op, $left, $right);
1548 17
            if ($out !== null) {
1549 17
                return $out;
1550
            }
1551
        }
1552
1553
        // make the expression look it did before being parsed
1554 1
        $paddedOp = $op;
1555 1
        if ($whiteBefore) {
1556 1
            $paddedOp = " " . $paddedOp;
1557
        }
1558 1
        if ($whiteAfter) {
1559 1
            $paddedOp .= " ";
1560
        }
1561
1562 1
        return ["string", "", [$left, $paddedOp, $right]];
1563
    }
1564
1565
    /**
1566
     * @param array $left
1567
     * @param array $right
1568
     *
1569
     * @return array|null
1570
     */
1571
    protected function stringConcatenate(array $left, array $right)
1572
    {
1573 19
        $strLeft = $this->coerce->coerceString($left);
1574 19
        if ($strLeft !== null) {
1575 2
            if ($right[0] === "string") {
1576 2
                $right[1] = "";
1577
            }
1578 2
            $strLeft[2][] = $right;
1579
1580 2
            return $strLeft;
1581
        }
1582
1583 18
        $strRight = $this->coerce->coerceString($right);
1584 18
        if ($strRight !== null) {
1585 1
            array_unshift($strRight[2], $left);
1586
1587 1
            return $strRight;
1588
        }
1589
1590 17
        return null;
1591
    }
1592
1593
1594
    /**
1595
     * make sure a color's components don't go out of bounds
1596
     *
1597
     * @param array $c
1598
     *
1599
     * @return mixed
1600
     */
1601
    public function fixColor(array $c)
1602
    {
1603 7
        foreach (range(1, 3) as $i) {
1604 7
            if ($c[$i] < 0) {
1605
                $c[$i] = 0;
1606
            }
1607 7
            if ($c[$i] > 255) {
1608 7
                $c[$i] = 255;
1609
            }
1610
        }
1611
1612 7
        return $c;
1613
    }
1614
1615
    /**
1616
     * @param string $op
1617
     * @param array  $lft
1618
     * @param array  $rgt
1619
     *
1620
     * @return array|null
1621
     * @throws \LesserPhp\Exception\GeneralException
1622
     */
1623
    protected function op_number_color($op, array $lft, array $rgt)
1624
    {
1625 1
        if ($op === '+' || $op === '*') {
1626 1
            return $this->op_color_number($op, $rgt, $lft);
1627
        }
1628
1629 1
        return null;
1630
    }
1631
1632
    /**
1633
     * @param string $op
1634
     * @param array  $lft
1635
     * @param array  $rgt
1636
     *
1637
     * @return array
1638
     * @throws \LesserPhp\Exception\GeneralException
1639
     */
1640
    protected function op_color_number($op, array $lft, array $rgt)
1641
    {
1642 2
        if ($rgt[0] === '%') {
1643
            $rgt[1] /= 100;
1644
        }
1645
1646 2
        return $this->op_color_color(
1647
            $op,
1648
            $lft,
1649 2
            array_fill(1, count($lft) - 1, $rgt[1])
1650
        );
1651
    }
1652
1653
    /**
1654
     * @param string $op
1655
     * @param        array
1656
     * $left
1657
     * @param array  $right
1658
     *
1659
     * @return array
1660
     * @throws \LesserPhp\Exception\GeneralException
1661
     */
1662
    protected function op_color_color($op, array $left, array $right)
1663
    {
1664 5
        $out = ['color'];
1665 5
        $max = count($left) > count($right) ? count($left) : count($right);
1666 5
        foreach (range(1, $max - 1) as $i) {
1667 5
            $lval = isset($left[$i]) ? $left[$i] : 0;
1668 5
            $rval = isset($right[$i]) ? $right[$i] : 0;
1669
            switch ($op) {
1670 5
                case '+':
1671 5
                    $out[] = $lval + $rval;
1672 5
                    break;
1673 1
                case '-':
1674 1
                    $out[] = $lval - $rval;
1675 1
                    break;
1676 1
                case '*':
1677 1
                    $out[] = $lval * $rval;
1678 1
                    break;
1679 1
                case '%':
1680 1
                    $out[] = $lval % $rval;
1681 1
                    break;
1682 1
                case '/':
1683 1
                    if ($rval == 0) {
1684
                        throw new GeneralException("evaluate error: can't divide by zero");
1685
                    }
1686 1
                    $out[] = $lval / $rval;
1687 1
                    break;
1688
                default:
1689 5
                    throw new GeneralException('evaluate error: color op number failed on op ' . $op);
1690
            }
1691
        }
1692
1693 5
        return $this->fixColor($out);
1694
    }
1695
1696
    /**
1697
     * operator on two numbers
1698
     *
1699
     * @param string $op
1700
     * @param array  $left
1701
     * @param array  $right
1702
     *
1703
     * @return array
1704
     * @throws \LesserPhp\Exception\GeneralException
1705
     */
1706
    protected function op_number_number($op, $left, $right)
1707
    {
1708 15
        $unit = empty($left[2]) ? $right[2] : $left[2];
1709
1710 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...
1711
        switch ($op) {
1712 15
            case '+':
1713 9
                $value = $left[1] + $right[1];
1714 9
                break;
1715 12
            case '*':
1716 8
                $value = $left[1] * $right[1];
1717 8
                break;
1718 10
            case '-':
1719 6
                $value = $left[1] - $right[1];
1720 6
                break;
1721 9
            case '%':
1722 1
                $value = $left[1] % $right[1];
1723 1
                break;
1724 8
            case '/':
1725 4
                if ($right[1] == 0) {
1726
                    throw new GeneralException('parse error: divide by zero');
1727
                }
1728 4
                $value = $left[1] / $right[1];
1729 4
                break;
1730 5
            case '<':
1731 1
                return $this->toBool($left[1] < $right[1]);
1732 5
            case '>':
1733 3
                return $this->toBool($left[1] > $right[1]);
1734 3
            case '>=':
1735 1
                return $this->toBool($left[1] >= $right[1]);
1736 2
            case '=<':
1737 2
                return $this->toBool($left[1] <= $right[1]);
1738
            default:
1739
                throw new GeneralException('parse error: unknown number operator: ' . $op);
1740
        }
1741
1742 13
        return ['number', $value, $unit];
1743
    }
1744
1745
    /**
1746
     * @param      $type
1747
     * @param null $selectors
1748
     *
1749
     * @return \stdClass
1750
     */
1751
    protected function makeOutputBlock($type, $selectors = null)
1752
    {
1753 49
        $b = new \stdClass();
1754 49
        $b->lines = [];
1755 49
        $b->children = [];
1756 49
        $b->selectors = $selectors;
1757 49
        $b->type = $type;
1758 49
        $b->parent = $this->scope;
1759
1760 49
        return $b;
1761
    }
1762
1763
    /**
1764
     * @param      $parent
1765
     * @param null $block
1766
     *
1767
     * @return \LesserPhp\NodeEnv
1768
     */
1769
    protected function pushEnv($parent, $block = null)
1770
    {
1771 49
        $e = new \LesserPhp\NodeEnv();
1772 49
        $e->setParent($parent);
1773 49
        $e->setBlock($block);
1774 49
        $e->setStore([]);
1775
1776 49
        $this->env = $e;
1777
1778 49
        return $e;
1779
    }
1780
1781
    /**
1782
     * pop something off the stack
1783
     *
1784
     * @return \LesserPhp\NodeEnv
1785
     */
1786
    protected function popEnv()
1787
    {
1788 40
        $old = $this->env;
1789 40
        $this->env = $this->env->getParent();
1790
1791 40
        return $old;
1792
    }
1793
1794
    /**
1795
     * set something in the current env
1796
     *
1797
     * @param $name
1798
     * @param $value
1799
     */
1800
    protected function set($name, $value)
1801
    {
1802 27
        $this->env->addStore($name, $value);
1803 27
    }
1804
1805
    /**
1806
     * get the highest occurrence entry for a name
1807
     *
1808
     * @param $name
1809
     *
1810
     * @return array
1811
     * @throws \LesserPhp\Exception\GeneralException
1812
     */
1813
    protected function get($name)
1814
    {
1815 28
        $current = $this->env;
1816
1817
        // track scope to evaluate
1818 28
        $scopeSecondary = [];
1819
1820 28
        $isArguments = $name === $this->vPrefix . 'arguments';
1821 28
        while ($current) {
1822 28
            if ($isArguments && count($current->getArguments()) > 0) {
1823 3
                return ['list', ' ', $current->getArguments()];
1824
            }
1825
1826 28
            if (isset($current->getStore()[$name])) {
1827 27
                return $current->getStore()[$name];
1828
            }
1829
            // has secondary scope?
1830 19
            if (isset($current->storeParent)) {
1831
                $scopeSecondary[] = $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...
1832
            }
1833
1834 19
            if ($current->getParent() !== null) {
1835 19
                $current = $current->getParent();
1836
            } else {
1837 1
                $current = null;
1838
            }
1839
        }
1840
1841 1
        while (count($scopeSecondary)) {
1842
            // pop one off
1843
            $current = array_shift($scopeSecondary);
1844
            while ($current) {
1845
                if ($isArguments && isset($current->arguments)) {
1846
                    return ['list', ' ', $current->arguments];
1847
                }
1848
1849
                if (isset($current->store[$name])) {
1850
                    return $current->store[$name];
1851
                }
1852
1853
                // has secondary scope?
1854
                if (isset($current->storeParent)) {
1855
                    $scopeSecondary[] = $current->storeParent;
1856
                }
1857
1858
                if (isset($current->parent)) {
1859
                    $current = $current->parent;
1860
                } else {
1861
                    $current = null;
1862
                }
1863
            }
1864
        }
1865
1866 1
        throw new GeneralException("variable $name is undefined");
1867
    }
1868
1869
    /**
1870
     * inject array of unparsed strings into environment as variables
1871
     *
1872
     * @param array $args
1873
     *
1874
     * @throws \LesserPhp\Exception\GeneralException
1875
     */
1876
    protected function injectVariables(array $args)
1877
    {
1878 1
        $this->pushEnv($this->env);
1879 1
        $parser = new Parser($this, __METHOD__);
1880 1
        foreach ($args as $name => $strValue) {
1881 1
            if ($name{0} !== '@') {
1882 1
                $name = '@' . $name;
1883
            }
1884 1
            $parser->count = 0;
1885 1
            $parser->buffer = (string)$strValue;
1886 1
            if (!$parser->propertyValue($value)) {
1887
                throw new GeneralException("failed to parse passed in variable $name: $strValue");
1888
            }
1889
1890 1
            $this->set($name, $value);
1891
        }
1892 1
    }
1893
1894
    /**
1895
     * @param string $string
1896
     * @param string $name
1897
     *
1898
     * @return string
1899
     * @throws \LesserPhp\Exception\GeneralException
1900
     */
1901
    public function compile($string, $name = null)
1902
    {
1903 49
        $locale = setlocale(LC_NUMERIC, 0);
1904 49
        setlocale(LC_NUMERIC, 'C');
1905
1906 49
        $this->parser = $this->makeParser($name);
1907 49
        $root = $this->parser->parse($string);
1908
1909 49
        $this->env = null;
1910 49
        $this->scope = null;
1911 49
        $this->allParsedFiles = [];
1912
1913 49
        $this->formatter = $this->newFormatter();
1914
1915 49
        if (!empty($this->registeredVars)) {
1916 1
            $this->injectVariables($this->registeredVars);
1917
        }
1918
1919 49
        $this->sourceParser = $this->parser; // used for error messages
1920 49
        $this->compileBlock($root);
1921
1922 38
        ob_start();
1923 38
        $this->formatter->block($this->scope);
1924 38
        $out = ob_get_clean();
1925 38
        setlocale(LC_NUMERIC, $locale);
1926
1927 38
        return $out;
1928
    }
1929
1930
    /**
1931
     * @param string $fname
1932
     * @param string $outFname
1933
     *
1934
     * @return int|string
1935
     * @throws \LesserPhp\Exception\GeneralException
1936
     */
1937
    public function compileFile($fname, $outFname = null)
1938
    {
1939 1
        if (!is_readable($fname)) {
1940
            throw new GeneralException('load error: failed to find ' . $fname);
1941
        }
1942
1943 1
        $pi = pathinfo($fname);
1944
1945 1
        $oldImport = $this->importDirs;
1946
1947 1
        $this->importDirs[] = $pi['dirname'] . '/';
1948
1949 1
        $this->addParsedFile($fname);
1950
1951 1
        $out = $this->compile(file_get_contents($fname), $fname);
1952
1953 1
        $this->importDirs = $oldImport;
1954
1955 1
        if ($outFname !== null) {
1956
            return file_put_contents($outFname, $out);
1957
        }
1958
1959 1
        return $out;
1960
    }
1961
1962
    /**
1963
     * Based on explicit input/output files does a full change check on cache before compiling.
1964
     *
1965
     * @param string  $in
1966
     * @param string  $out
1967
     * @param boolean $force
1968
     *
1969
     * @return string Compiled CSS results
1970
     * @throws GeneralException
1971
     */
1972
    public function checkedCachedCompile($in, $out, $force = false)
1973
    {
1974 1
        if (!is_file($in) || !is_readable($in)) {
1975
            throw new GeneralException('Invalid or unreadable input file specified.');
1976
        }
1977 1
        if (is_dir($out) || !is_writable(file_exists($out) ? $out : dirname($out))) {
1978
            throw new GeneralException('Invalid or unwritable output file specified.');
1979
        }
1980
1981 1
        $outMeta = $out . '.meta';
1982 1
        $metadata = null;
1983 1
        if (!$force && is_file($outMeta)) {
1984
            $metadata = unserialize(file_get_contents($outMeta));
1985
        }
1986
1987 1
        $output = $this->cachedCompile($metadata ?: $in);
1988
1989 1
        if (!$metadata || $metadata['updated'] != $output['updated']) {
1990 1
            $css = $output['compiled'];
1991 1
            unset($output['compiled']);
1992 1
            file_put_contents($out, $css);
1993 1
            file_put_contents($outMeta, serialize($output));
1994
        } else {
1995
            $css = file_get_contents($out);
1996
        }
1997
1998 1
        return $css;
1999
    }
2000
2001
    /**
2002
     * compile only if changed input has changed or output doesn't exist
2003
     *
2004
     * @param string $in
2005
     * @param string $out
2006
     *
2007
     * @return bool
2008
     * @throws \LesserPhp\Exception\GeneralException
2009
     */
2010
    public function checkedCompile($in, $out)
2011
    {
2012
        if (!is_file($out) || filemtime($in) > filemtime($out)) {
2013
            $this->compileFile($in, $out);
2014
2015
            return true;
2016
        }
2017
2018
        return false;
2019
    }
2020
2021
    /**
2022
     * Execute lessphp on a .less file or a lessphp cache structure
2023
     *
2024
     * The lessphp cache structure contains information about a specific
2025
     * less file having been parsed. It can be used as a hint for future
2026
     * calls to determine whether or not a rebuild is required.
2027
     *
2028
     * The cache structure contains two important keys that may be used
2029
     * externally:
2030
     *
2031
     * compiled: The final compiled CSS
2032
     * updated: The time (in seconds) the CSS was last compiled
2033
     *
2034
     * The cache structure is a plain-ol' PHP associative array and can
2035
     * be serialized and unserialized without a hitch.
2036
     *
2037
     * @param mixed $in    Input
2038
     * @param bool  $force Force rebuild?
2039
     *
2040
     * @return array lessphp cache structure
2041
     * @throws \LesserPhp\Exception\GeneralException
2042
     */
2043
    public function cachedCompile($in, $force = false)
2044
    {
2045
        // assume no root
2046 1
        $root = null;
2047
2048 1
        if (is_string($in)) {
2049 1
            $root = $in;
2050
        } elseif (is_array($in) && isset($in['root'])) {
2051
            if ($force || !isset($in['files'])) {
2052
                // If we are forcing a recompile or if for some reason the
2053
                // structure does not contain any file information we should
2054
                // specify the root to trigger a rebuild.
2055
                $root = $in['root'];
2056
            } elseif (isset($in['files']) && is_array($in['files'])) {
2057
                foreach ($in['files'] as $fname => $ftime) {
2058
                    if (!file_exists($fname) || filemtime($fname) > $ftime) {
2059
                        // One of the files we knew about previously has changed
2060
                        // so we should look at our incoming root again.
2061
                        $root = $in['root'];
2062
                        break;
2063
                    }
2064
                }
2065
            }
2066
        } else {
2067
            // TODO: Throw an exception? We got neither a string nor something
2068
            // that looks like a compatible lessphp cache structure.
2069
            return null;
2070
        }
2071
2072 1
        if ($root !== null) {
2073
            // If we have a root value which means we should rebuild.
2074 1
            $out = [];
2075 1
            $out['root'] = $root;
2076 1
            $out['compiled'] = $this->compileFile($root);
2077 1
            $out['files'] = $this->allParsedFiles;
2078 1
            $out['updated'] = time();
2079
2080 1
            return $out;
2081
        } else {
2082
            // No changes, pass back the structure
2083
            // we were given initially.
2084
            return $in;
2085
        }
2086
    }
2087
2088
    /**
2089
     * parse and compile buffer
2090
     * This is deprecated
2091
     *
2092
     * @param null $str
2093
     * @param null $initialVariables
2094
     *
2095
     * @return int|string
2096
     * @throws \LesserPhp\Exception\GeneralException
2097
     * @deprecated
2098
     */
2099
    public function parse($str = null, $initialVariables = null)
2100
    {
2101 37
        if (is_array($str)) {
2102
            $initialVariables = $str;
2103
            $str = null;
2104
        }
2105
2106 37
        $oldVars = $this->registeredVars;
2107 37
        if ($initialVariables !== null) {
2108 1
            $this->setVariables($initialVariables);
2109
        }
2110
2111 37
        if ($str === null) {
2112
            throw new GeneralException('nothing to parse');
2113
        } else {
2114 37
            $out = $this->compile($str);
2115
        }
2116
2117 37
        $this->registeredVars = $oldVars;
2118
2119 37
        return $out;
2120
    }
2121
2122
    /**
2123
     * @param string $name
2124
     *
2125
     * @return \LesserPhp\Parser
2126
     */
2127
    protected function makeParser($name)
2128
    {
2129 49
        $parser = new Parser($this, $name);
2130 49
        $parser->setWriteComments($this->preserveComments);
2131
2132 49
        return $parser;
2133
    }
2134
2135
    /**
2136
     * @param string $name
2137
     */
2138
    public function setFormatter($name)
2139
    {
2140 1
        $this->formatterName = $name;
2141 1
    }
2142
2143
    /**
2144
     * @return \LesserPhp\Formatter\FormatterInterface
2145
     */
2146
    protected function newFormatter()
2147
    {
2148 49
        $className = 'Lessjs';
2149 49
        if (!empty($this->formatterName)) {
2150 1
            if (!is_string($this->formatterName)) {
2151
                return $this->formatterName;
2152
            }
2153 1
            $className = $this->formatterName;
2154
        }
2155
2156 49
        $className = '\LesserPhp\Formatter\\' . $className;
2157
2158 49
        return new $className;
2159
    }
2160
2161
    /**
2162
     * @param bool $preserve
2163
     */
2164
    public function setPreserveComments($preserve)
2165
    {
2166 1
        $this->preserveComments = $preserve;
2167 1
    }
2168
2169
    /**
2170
     * @param string   $name
2171
     * @param callable $func
2172
     */
2173
    public function registerFunction($name, callable $func)
2174
    {
2175 1
        $this->libFunctions[$name] = $func;
2176 1
    }
2177
2178
    /**
2179
     * @param string $name
2180
     */
2181
    public function unregisterFunction($name)
2182
    {
2183 1
        unset($this->libFunctions[$name]);
2184 1
    }
2185
2186
    /**
2187
     * @param array $variables
2188
     */
2189
    public function setVariables(array $variables)
2190
    {
2191 1
        $this->registeredVars = array_merge($this->registeredVars, $variables);
0 ignored issues
show
Documentation Bug introduced by
It seems like array_merge($this->registeredVars, $variables) of type array is incompatible with the declared type array<integer,string> of property $registeredVars.

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...
2192 1
    }
2193
2194
    /**
2195
     * @param $name
2196
     */
2197
    public function unsetVariable($name)
2198
    {
2199
        unset($this->registeredVars[$name]);
2200
    }
2201
2202
    /**
2203
     * @param string[] $dirs
2204
     */
2205
    public function setImportDirs(array $dirs)
2206
    {
2207 38
        $this->importDirs = $dirs;
2208 38
    }
2209
2210
    /**
2211
     * @param string $dir
2212
     */
2213
    public function addImportDir($dir)
2214
    {
2215
        $this->importDirs[] = $dir;
2216
    }
2217
2218
    /**
2219
     * @return string[]
2220
     */
2221
    public function getImportDirs()
2222
    {
2223 1
        return $this->importDirs;
2224
    }
2225
2226
    /**
2227
     * @param string $file
2228
     */
2229
    public function addParsedFile($file)
2230
    {
2231 2
        $this->allParsedFiles[realpath($file)] = filemtime($file);
2232 2
    }
2233
2234
    /**
2235
     * Uses the current value of $this->count to show line and line number
2236
     *
2237
     * @param string $msg
2238
     *
2239
     * @throws GeneralException
2240
     */
2241
    public function throwError($msg = null)
2242
    {
2243
        if ($this->sourceLoc >= 0) {
2244
            $this->sourceParser->throwError($msg, $this->sourceLoc);
2245
        }
2246
        throw new GeneralException($msg);
2247
    }
2248
2249
    /**
2250
     * compile file $in to file $out if $in is newer than $out
2251
     * returns true when it compiles, false otherwise
2252
     *
2253
     * @param                          $in
2254
     * @param                          $out
2255
     * @param \LesserPhp\Compiler|null $less
2256
     *
2257
     * @return bool
2258
     * @throws \LesserPhp\Exception\GeneralException
2259
     */
2260
    public static function ccompile($in, $out, Compiler $less = null)
2261
    {
2262
        if ($less === null) {
2263
            $less = new self;
2264
        }
2265
2266
        return $less->checkedCompile($in, $out);
2267
    }
2268
2269
    /**
2270
     * @param                          $in
2271
     * @param bool                     $force
2272
     * @param \LesserPhp\Compiler|null $less
2273
     *
2274
     * @return array
2275
     * @throws \LesserPhp\Exception\GeneralException
2276
     */
2277
    public static function cexecute($in, $force = false, Compiler $less = null)
2278
    {
2279
        if ($less === null) {
2280
            $less = new self;
2281
        }
2282
2283
        return $less->cachedCompile($in, $force);
2284
    }
2285
2286
    /**
2287
     * prefix of abstract properties
2288
     *
2289
     * @return string
2290
     */
2291
    public function getVPrefix()
2292
    {
2293 49
        return $this->vPrefix;
2294
    }
2295
2296
    /**
2297
     * prefix of abstract blocks
2298
     *
2299
     * @return string
2300
     */
2301
    public function getMPrefix()
2302
    {
2303 46
        return $this->mPrefix;
2304
    }
2305
2306
    /**
2307
     * @return string
2308
     */
2309
    public function getParentSelector()
2310
    {
2311 3
        return $this->parentSelector;
2312
    }
2313
2314
    /**
2315
     * @param int $numberPresicion
2316
     */
2317
    protected function setNumberPrecision($numberPresicion = null)
2318
    {
2319
        $this->numberPrecision = $numberPresicion;
2320
    }
2321
2322
    /**
2323
     * @return \LesserPhp\Library\Coerce
2324
     */
2325
    protected function getCoerce()
2326
    {
2327
        return $this->coerce;
2328
    }
2329
2330
    public function setImportDisabled()
2331
    {
2332 1
        $this->importDisabled = true;
2333 1
    }
2334
2335
    /**
2336
     * @return bool
2337
     */
2338
    public function isImportDisabled()
2339
    {
2340 3
        return $this->importDisabled;
2341
    }
2342
}
2343