Completed
Push — master ( b38731...5d7e29 )
by Marcus
02:03
created

Compiler   F

Complexity

Total Complexity 360

Size/Duplication

Total Lines 2213
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 91.6%

Importance

Changes 0
Metric Value
wmc 360
lcom 1
cbo 11
dl 0
loc 2213
rs 3.9999
c 0
b 0
f 0
ccs 818
cts 893
cp 0.916

73 Methods

Rating   Name   Duplication   Size   Complexity  
A compileImportedProps() 0 15 2
A __construct() 0 7 1
A compressList() 0 8 3
A pregQuote() 0 4 1
C tryImport() 0 65 12
B compileBlock() 0 24 6
A compileCSSBlock() 0 14 1
B compileMedia() 0 25 3
A mediaParent() 0 11 4
A compileNestedBlock() 0 11 1
A compileRoot() 0 7 1
A compileProps() 0 7 2
A deduplicate() 0 19 4
C sortProps() 0 44 8
D compileMediaQuery() 0 37 9
C multiplyMedia() 0 27 8
A expandParentSelectors() 0 12 2
A findClosestSelectors() 0 14 3
B multiplySelectors() 0 30 6
A compileSelectors() 0 15 3
A equals() 0 4 1
F patternMatch() 0 88 25
B patternMatchAll() 0 16 5
C findBlocks() 0 48 11
C zipSetArgs() 0 40 7
F compileProp() 0 145 34
A compileValue() 0 19 4
A colorArgs() 0 11 3
D reduce() 0 148 35
A flattenList() 0 8 3
C evaluate() 0 54 12
A stringConcatenate() 0 21 4
A fixColor() 0 13 4
A op_number_color() 0 8 3
A op_color_number() 0 12 2
C op_color_color() 0 33 11
C op_number_number() 0 38 12
A makeOutputBlock() 0 11 1
A pushEnv() 0 11 1
A popEnv() 0 7 1
A set() 0 4 1
C get() 0 55 14
A injectVariables() 0 17 4
B compile() 0 28 2
B compileFile() 0 24 3
C checkedCachedCompile() 0 28 11
A checkedCompile() 0 10 3
C cachedCompile() 0 44 12
B parse() 0 22 4
A makeParser() 0 7 1
A setFormatter() 0 4 1
A setFormatterClass() 0 4 1
A newFormatter() 0 14 3
A setPreserveComments() 0 4 1
A registerFunction() 0 4 1
A unregisterFunction() 0 4 1
A setVariables() 0 4 1
A unsetVariable() 0 4 1
A setImportDirs() 0 4 1
A addImportDir() 0 4 1
A getImportDirs() 0 4 1
A addParsedFile() 0 4 1
A throwError() 0 7 2
A ccompile() 0 8 2
A cexecute() 0 8 2
A getVPrefix() 0 4 1
A getMPrefix() 0 4 1
A getParentSelector() 0 4 1
A setNumberPrecision() 0 4 1
A getCoerce() 0 4 1
A setImportDisabled() 0 4 1
A isImportDisabled() 0 4 1
C funcToColor() 0 66 19

How to fix   Complexity   

Complex Class

Complex classes like Compiler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Compiler, and based on these observations, apply Extract Interface, too.

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

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
336 3
                break;
337 4
            case "directive":
338 4
                $name = "@" . $block->name;
0 ignored issues
show
Bug introduced by
The property name does not seem to exist in LesserPhp\Block.

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...
339 4
                if (!empty($block->value)) {
340 2
                    $name .= " " . $this->compileValue($this->reduce($block->value));
0 ignored issues
show
Bug introduced by
The property value does not seem to exist in LesserPhp\Block.

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...
341
                }
342
343 4
                $this->compileNestedBlock($block, [$name]);
344 4
                break;
345
            default:
346
                $block->parser->throwError("unknown block type: $block->type\n", $block->count);
347
        }
348 38
    }
349
350
    /**
351
     * @param Block $block
352
     *
353
     * @throws \LesserPhp\Exception\GeneralException
354
     */
355 46
    protected function compileCSSBlock(Block $block)
356
    {
357 46
        $env = $this->pushEnv($this->env);
358
359 46
        $selectors = $this->compileSelectors($block->tags);
0 ignored issues
show
Bug introduced by
It seems like $block->tags can also be of type null; however, LesserPhp\Compiler::compileSelectors() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
360 46
        $env->setSelectors($this->multiplySelectors($selectors));
361 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...
362
363 46
        $this->scope->children[] = $out;
364 46
        $this->compileProps($block, $out);
365
366 35
        $block->scope = $env; // mixins carry scope with them!
0 ignored issues
show
Bug introduced by
The property scope does not seem to exist in LesserPhp\Block.

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...
367 35
        $this->popEnv();
368 35
    }
369
370
    /**
371
     * @param Block\Media $media
372
     */
373 3
    protected function compileMedia(Block\Media $media)
374
    {
375 3
        $env = $this->pushEnv($this->env, $media);
376 3
        $parentScope = $this->mediaParent($this->scope);
377
378 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...
379
380 3
        $this->scope = $this->makeOutputBlock($media->type, [$query]);
381 3
        $parentScope->children[] = $this->scope;
382
383 3
        $this->compileProps($media, $this->scope);
384
385 3
        if (count($this->scope->lines) > 0) {
386 3
            $orphanSelelectors = $this->findClosestSelectors();
387 3
            if ($orphanSelelectors !== null) {
388 3
                $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
389 3
                $orphan->lines = $this->scope->lines;
390 3
                array_unshift($this->scope->children, $orphan);
391 3
                $this->scope->lines = [];
392
            }
393
        }
394
395 3
        $this->scope = $this->scope->parent;
396 3
        $this->popEnv();
397 3
    }
398
399
    /**
400
     * @param $scope
401
     *
402
     * @return mixed
403
     */
404 3
    protected function mediaParent($scope)
405
    {
406 3
        while (!empty($scope->parent)) {
407 1
            if (!empty($scope->type) && $scope->type !== "media") {
408 1
                break;
409
            }
410 1
            $scope = $scope->parent;
411
        }
412
413 3
        return $scope;
414
    }
415
416
    /**
417
     * @param Block    $block
418
     * @param string[] $selectors
419
     */
420 4
    protected function compileNestedBlock(Block $block, array $selectors)
421
    {
422 4
        $this->pushEnv($this->env, $block);
423 4
        $this->scope = $this->makeOutputBlock($block->type, $selectors);
424 4
        $this->scope->parent->children[] = $this->scope;
425
426 4
        $this->compileProps($block, $this->scope);
427
428 4
        $this->scope = $this->scope->parent;
429 4
        $this->popEnv();
430 4
    }
431
432
    /**
433
     * @param Block $root
434
     */
435 49
    protected function compileRoot(Block $root)
436
    {
437 49
        $this->pushEnv($this->env);
438 49
        $this->scope = $this->makeOutputBlock($root->type);
439 49
        $this->compileProps($root, $this->scope);
440 38
        $this->popEnv();
441 38
    }
442
443
    /**
444
     * @param Block $block
445
     * @param \stdClass $out
446
     *
447
     * @throws \LesserPhp\Exception\GeneralException
448
     */
449 49
    protected function compileProps(Block $block, $out)
450
    {
451 49
        foreach ($this->sortProps($block->props) as $prop) {
452 49
            $this->compileProp($prop, $block, $out);
453
        }
454 38
        $out->lines = $this->deduplicate($out->lines);
455 38
    }
456
457
    /**
458
     * Deduplicate lines in a block. Comments are not deduplicated. If a
459
     * duplicate rule is detected, the comments immediately preceding each
460
     * occurence are consolidated.
461
     *
462
     * @param array $lines
463
     *
464
     * @return array
465
     */
466 38
    protected function deduplicate(array $lines)
467
    {
468 38
        $unique = [];
469 38
        $comments = [];
470
471 38
        foreach ($lines as $line) {
472 38
            if (strpos($line, '/*') === 0) {
473 2
                $comments[] = $line;
474 2
                continue;
475
            }
476 37
            if (!in_array($line, $unique)) {
477 37
                $unique[] = $line;
478
            }
479 37
            array_splice($unique, array_search($line, $unique), 0, $comments);
480 37
            $comments = [];
481
        }
482
483 38
        return array_merge($unique, $comments);
484
    }
485
486
    /**
487
     * @param array $props
488
     * @param bool  $split
489
     *
490
     * @return array
491
     */
492 49
    protected function sortProps(array $props, $split = false)
493
    {
494 49
        $vars = [];
495 49
        $imports = [];
496 49
        $other = [];
497 49
        $stack = [];
498
499 49
        foreach ($props as $prop) {
500 49
            switch ($prop[0]) {
501 49
                case "comment":
502 1
                    $stack[] = $prop;
503 1
                    break;
504 49
                case "assign":
505 43
                    $stack[] = $prop;
506 43
                    if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
507 23
                        $vars = array_merge($vars, $stack);
508
                    } else {
509 43
                        $other = array_merge($other, $stack);
510
                    }
511 43
                    $stack = [];
512 43
                    break;
513 47
                case "import":
514 3
                    $id = self::$nextImportId++;
515 3
                    $prop[] = $id;
516 3
                    $stack[] = $prop;
517 3
                    $imports = array_merge($imports, $stack);
518 3
                    $other[] = ["import_mixin", $id];
519 3
                    $stack = [];
520 3
                    break;
521
                default:
522 46
                    $stack[] = $prop;
523 46
                    $other = array_merge($other, $stack);
524 46
                    $stack = [];
525 49
                    break;
526
            }
527
        }
528 49
        $other = array_merge($other, $stack);
529
530 49
        if ($split) {
531 2
            return [array_merge($imports, $vars), $other];
532
        } else {
533 49
            return array_merge($imports, $vars, $other);
534
        }
535
    }
536
537
    /**
538
     * @param array $queries
539
     *
540
     * @return string
541
     */
542 3
    protected function compileMediaQuery(array $queries)
543
    {
544 3
        $compiledQueries = [];
545 3
        foreach ($queries as $query) {
546 3
            $parts = [];
547 3
            foreach ($query as $q) {
548 3
                switch ($q[0]) {
549 3
                    case "mediaType":
550 3
                        $parts[] = implode(" ", array_slice($q, 1));
551 3
                        break;
552 1
                    case "mediaExp":
553 1
                        if (isset($q[2])) {
554 1
                            $parts[] = "($q[1]: " .
555 1
                                $this->compileValue($this->reduce($q[2])) . ")";
556
                        } else {
557 1
                            $parts[] = "($q[1])";
558
                        }
559 1
                        break;
560 1
                    case "variable":
561 1
                        $parts[] = $this->compileValue($this->reduce($q));
562 3
                        break;
563
                }
564
            }
565
566 3
            if (count($parts) > 0) {
567 3
                $compiledQueries[] = implode(" and ", $parts);
568
            }
569
        }
570
571 3
        $out = "@media";
572 3
        if (!empty($parts)) {
573
            $out .= " " .
574 3
                implode($this->formatter->getSelectorSeparator(), $compiledQueries);
575
        }
576
577 3
        return $out;
578
    }
579
580
    /**
581
     * @param \LesserPhp\NodeEnv $env
582
     * @param array              $childQueries
583
     *
584
     * @return array
585
     */
586 3
    protected function multiplyMedia(NodeEnv $env = null, array $childQueries = null)
587
    {
588 3
        if ($env === null ||
589 3
            (!empty($env->getBlock()->type) && $env->getBlock()->type !== 'media')
590
        ) {
591 3
            return $childQueries;
592
        }
593
594
        // plain old block, skip
595 3
        if (empty($env->getBlock()->type)) {
596 3
            return $this->multiplyMedia($env->getParent(), $childQueries);
597
        }
598
599 3
        $out = [];
600 3
        $queries = $env->getBlock()->queries;
601 3
        if ($childQueries === null) {
602 3
            $out = $queries;
603
        } else {
604 1
            foreach ($queries as $parent) {
605 1
                foreach ($childQueries as $child) {
606 1
                    $out[] = array_merge($parent, $child);
607
                }
608
            }
609
        }
610
611 3
        return $this->multiplyMedia($env->getParent(), $out);
612
    }
613
614
    /**
615
     * @param $tag
616
     * @param $replace
617
     *
618
     * @return int
619
     */
620 46
    protected function expandParentSelectors(&$tag, $replace)
621
    {
622 46
        $parts = explode("$&$", $tag);
623 46
        $count = 0;
624 46
        foreach ($parts as &$part) {
625 46
            $part = str_replace($this->parentSelector, $replace, $part, $c);
626 46
            $count += $c;
627
        }
628 46
        $tag = implode($this->parentSelector, $parts);
629
630 46
        return $count;
631
    }
632
633
    /**
634
     * @return array|null
635
     */
636 46
    protected function findClosestSelectors()
637
    {
638 46
        $env = $this->env;
639 46
        $selectors = null;
640 46
        while ($env !== null) {
641 46
            if ($env->getSelectors() !== null) {
642 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...
643 13
                break;
644
            }
645 46
            $env = $env->getParent();
646
        }
647
648 46
        return $selectors;
649
    }
650
651
652
    /**
653
     *  multiply $selectors against the nearest selectors in env
654
     *
655
     * @param array $selectors
656
     *
657
     * @return array
658
     */
659 46
    protected function multiplySelectors(array $selectors)
660
    {
661
        // find parent selectors
662
663 46
        $parentSelectors = $this->findClosestSelectors();
664 46
        if ($parentSelectors === null) {
665
            // kill parent reference in top level selector
666 46
            foreach ($selectors as &$s) {
667 46
                $this->expandParentSelectors($s, "");
668
            }
669
670 46
            return $selectors;
671
        }
672
673 13
        $out = [];
674 13
        foreach ($parentSelectors as $parent) {
675 13
            foreach ($selectors as $child) {
676 13
                $count = $this->expandParentSelectors($child, $parent);
677
678
                // don't prepend the parent tag if & was used
679 13
                if ($count > 0) {
680 4
                    $out[] = trim($child);
681
                } else {
682 13
                    $out[] = trim($parent . ' ' . $child);
683
                }
684
            }
685
        }
686
687 13
        return $out;
688
    }
689
690
    /**
691
     * reduces selector expressions
692
     *
693
     * @param array $selectors
694
     *
695
     * @return array
696
     * @throws \LesserPhp\Exception\GeneralException
697
     */
698 46
    protected function compileSelectors(array $selectors)
699
    {
700 46
        $out = [];
701
702 46
        foreach ($selectors as $s) {
703 46
            if (is_array($s)) {
704 4
                list(, $value) = $s;
705 4
                $out[] = trim($this->compileValue($this->reduce($value)));
706
            } else {
707 46
                $out[] = $s;
708
            }
709
        }
710
711 46
        return $out;
712
    }
713
714
    /**
715
     * @param $left
716
     * @param $right
717
     *
718
     * @return bool
719
     */
720 4
    protected function equals($left, $right)
721
    {
722 4
        return $left == $right;
723
    }
724
725
    /**
726
     * @param $block
727
     * @param $orderedArgs
728
     * @param $keywordArgs
729
     *
730
     * @return bool
731
     */
732 21
    protected function patternMatch($block, $orderedArgs, $keywordArgs)
733
    {
734
        // match the guards if it has them
735
        // any one of the groups must have all its guards pass for a match
736 21
        if (!empty($block->guards)) {
737 5
            $groupPassed = false;
738 5
            foreach ($block->guards as $guardGroup) {
739 5
                foreach ($guardGroup as $guard) {
740 5
                    $this->pushEnv($this->env);
741 5
                    $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
742
743 5
                    $negate = false;
744 5
                    if ($guard[0] === "negate") {
745 1
                        $guard = $guard[1];
746 1
                        $negate = true;
747
                    }
748
749 5
                    $passed = $this->reduce($guard) == self::$TRUE;
750 5
                    if ($negate) {
751 1
                        $passed = !$passed;
752
                    }
753
754 5
                    $this->popEnv();
755
756 5
                    if ($passed) {
757 3
                        $groupPassed = true;
758
                    } else {
759 5
                        $groupPassed = false;
760 5
                        break;
761
                    }
762
                }
763
764 5
                if ($groupPassed) {
765 5
                    break;
766
                }
767
            }
768
769 5
            if (!$groupPassed) {
770 5
                return false;
771
            }
772
        }
773
774 19
        if (empty($block->args)) {
775 12
            return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
776
        }
777
778 14
        $remainingArgs = $block->args;
779 14
        if ($keywordArgs) {
780 2
            $remainingArgs = [];
781 2
            foreach ($block->args as $arg) {
782 2
                if ($arg[0] === "arg" && isset($keywordArgs[$arg[1]])) {
783 2
                    continue;
784
                }
785
786 2
                $remainingArgs[] = $arg;
787
            }
788
        }
789
790 14
        $i = -1; // no args
791
        // try to match by arity or by argument literal
792 14
        foreach ($remainingArgs as $i => $arg) {
793 14
            switch ($arg[0]) {
794 14
                case "lit":
795 3
                    if (empty($orderedArgs[$i]) || !$this->equals($arg[1], $orderedArgs[$i])) {
796 2
                        return false;
797
                    }
798 3
                    break;
799 14
                case "arg":
800
                    // no arg and no default value
801 14
                    if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
802 3
                        return false;
803
                    }
804 14
                    break;
805 2
                case "rest":
806 2
                    $i--; // rest can be empty
807 14
                    break 2;
808
            }
809
        }
810
811 13
        if ($block->isVararg) {
812 2
            return true; // not having enough is handled above
813
        } else {
814 13
            $numMatched = $i + 1;
815
816
            // greater than because default values always match
817 13
            return $numMatched >= count($orderedArgs);
818
        }
819
    }
820
821
    /**
822
     * @param array $blocks
823
     * @param       $orderedArgs
824
     * @param       $keywordArgs
825
     * @param array $skip
826
     *
827
     * @return array|null
828
     */
829 21
    protected function patternMatchAll(array $blocks, $orderedArgs, $keywordArgs, array $skip = [])
830
    {
831 21
        $matches = null;
832 21
        foreach ($blocks as $block) {
833
            // skip seen blocks that don't have arguments
834 21
            if (isset($skip[$block->id]) && !isset($block->args)) {
835 1
                continue;
836
            }
837
838 21
            if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
839 21
                $matches[] = $block;
840
            }
841
        }
842
843 21
        return $matches;
844
    }
845
846
    /**
847
     * attempt to find blocks matched by path and args
848
     *
849
     * @param       $searchIn
850
     * @param array $path
851
     * @param       $orderedArgs
852
     * @param       $keywordArgs
853
     * @param array $seen
854
     *
855
     * @return array|null
856
     */
857 22
    protected function findBlocks($searchIn, array $path, $orderedArgs, $keywordArgs, array $seen = [])
858
    {
859 22
        if ($searchIn === null) {
860 5
            return null;
861
        }
862 22
        if (isset($seen[$searchIn->id])) {
863 1
            return null;
864
        }
865 22
        $seen[$searchIn->id] = true;
866
867 22
        $name = $path[0];
868
869 22
        if (isset($searchIn->children[$name])) {
870 21
            $blocks = $searchIn->children[$name];
871 21
            if (count($path) === 1) {
872 21
                $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
873 21
                if (!empty($matches)) {
874
                    // This will return all blocks that match in the closest
875
                    // scope that has any matching block, like lessjs
876 21
                    return $matches;
877
                }
878
            } else {
879 3
                $matches = [];
880 3
                foreach ($blocks as $subBlock) {
881 3
                    $subMatches = $this->findBlocks(
882
                        $subBlock,
883 3
                        array_slice($path, 1),
884
                        $orderedArgs,
885
                        $keywordArgs,
886
                        $seen
887
                    );
888
889 3
                    if ($subMatches !== null) {
890 3
                        foreach ($subMatches as $sm) {
891 3
                            $matches[] = $sm;
892
                        }
893
                    }
894
                }
895
896 3
                return count($matches) > 0 ? $matches : null;
897
            }
898
        }
899 22
        if ($searchIn->parent === $searchIn) {
900
            return null;
901
        }
902
903 22
        return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
904
    }
905
906
    /**
907
     * sets all argument names in $args to either the default value
908
     * or the one passed in through $values
909
     *
910
     * @param array $args
911
     * @param       $orderedValues
912
     * @param       $keywordValues
913
     *
914
     * @throws \LesserPhp\Exception\GeneralException
915
     */
916 16
    protected function zipSetArgs(array $args, $orderedValues, $keywordValues)
917
    {
918 16
        $assignedValues = [];
919
920 16
        $i = 0;
921 16
        foreach ($args as $a) {
922 14
            if ($a[0] === "arg") {
923 14
                if (isset($keywordValues[$a[1]])) {
924
                    // has keyword arg
925 2
                    $value = $keywordValues[$a[1]];
926 14
                } elseif (isset($orderedValues[$i])) {
927
                    // has ordered arg
928 13
                    $value = $orderedValues[$i];
929 13
                    $i++;
930 6
                } elseif (isset($a[2])) {
931
                    // has default value
932 6
                    $value = $a[2];
933
                } else {
934
                    throw new GeneralException('Failed to assign arg ' . $a[1]);
935
                }
936
937 14
                $value = $this->reduce($value);
938 14
                $this->set($a[1], $value);
939 14
                $assignedValues[] = $value;
940
            } else {
941
                // a lit
942 14
                $i++;
943
            }
944
        }
945
946
        // check for a rest
947 16
        $last = end($args);
948 16
        if ($last[0] === "rest") {
949 2
            $rest = array_slice($orderedValues, count($args) - 1);
950 2
            $this->set($last[1], $this->reduce(["list", " ", $rest]));
951
        }
952
953
        // wow is this the only true use of PHP's + operator for arrays?
954 16
        $this->env->setArguments($assignedValues + $orderedValues);
955 16
    }
956
957
    /**
958
     * compile a prop and update $lines or $blocks appropriately
959
     *
960
     * @param $prop
961
     * @param Block $block
962
     * @param $out
963
     *
964
     * @throws \LesserPhp\Exception\GeneralException
965
     */
966 49
    protected function compileProp($prop, Block $block, $out)
967
    {
968
        // set error position context
969 49
        $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
970
971 49
        switch ($prop[0]) {
972 49
            case 'assign':
973 43
                list(, $name, $value) = $prop;
974 43
                if ($name[0] == $this->vPrefix) {
975 23
                    $this->set($name, $value);
976
                } else {
977 43
                    $out->lines[] = $this->formatter->property(
978
                        $name,
979 43
                        $this->compileValue($this->reduce($value))
980
                    );
981
                }
982 37
                break;
983 47
            case 'block':
984 46
                list(, $child) = $prop;
985 46
                $this->compileBlock($child);
986 35
                break;
987 25
            case 'ruleset':
988 25
            case 'mixin':
989 22
                list(, $path, $args, $suffix) = $prop;
990
991 22
                $orderedArgs = [];
992 22
                $keywordArgs = [];
993 22
                foreach ((array)$args as $arg) {
994 15
                    switch ($arg[0]) {
995 15
                        case "arg":
996 4
                            if (!isset($arg[2])) {
997 3
                                $orderedArgs[] = $this->reduce(["variable", $arg[1]]);
998
                            } else {
999 2
                                $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
1000
                            }
1001 4
                            break;
1002
1003 15
                        case "lit":
1004 15
                            $orderedArgs[] = $this->reduce($arg[1]);
1005 15
                            break;
1006
                        default:
1007 15
                            throw new GeneralException("Unknown arg type: " . $arg[0]);
1008
                    }
1009
                }
1010
1011 22
                $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
1012
1013 22
                if ($mixins === null) {
1014 5
                    $block->parser->throwError("{$prop[1][0]} is undefined", $block->count);
1015
                }
1016
1017 17
                if (strpos($prop[1][0], "$") === 0) {
1018
                    //Use Ruleset Logic - Only last element
1019 8
                    $mixins = [array_pop($mixins)];
1020
                }
1021
1022 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...
1023 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...
1024
                        continue;
1025
                    }
1026
1027 17
                    $haveScope = false;
1028 17
                    if (isset($mixin->parent->scope)) {
1029 2
                        $haveScope = true;
1030 2
                        $mixinParentEnv = $this->pushEnv($this->env);
1031 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...
1032
                    }
1033
1034 17
                    $haveArgs = false;
1035 17
                    if (isset($mixin->args)) {
1036 14
                        $haveArgs = true;
1037 14
                        $this->pushEnv($this->env);
1038 14
                        $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
1039
                    }
1040
1041 17
                    $oldParent = $mixin->parent;
1042 17
                    if ($mixin != $block) {
1043 17
                        $mixin->parent = $block;
1044
                    }
1045
1046 17
                    foreach ($this->sortProps($mixin->props) as $subProp) {
1047 17
                        if ($suffix !== null &&
1048 17
                            $subProp[0] === "assign" &&
1049 17
                            is_string($subProp[1]) &&
1050 17
                            $subProp[1]{0} != $this->vPrefix
1051
                        ) {
1052 1
                            $subProp[2] = [
1053 1
                                'list',
1054 1
                                ' ',
1055 1
                                [$subProp[2], ['keyword', $suffix]],
1056
                            ];
1057
                        }
1058
1059 17
                        $this->compileProp($subProp, $mixin, $out);
1060
                    }
1061
1062 17
                    $mixin->parent = $oldParent;
1063
1064 17
                    if ($haveArgs) {
1065 14
                        $this->popEnv();
1066
                    }
1067 17
                    if ($haveScope) {
1068 17
                        $this->popEnv();
1069
                    }
1070
                }
1071
1072 17
                break;
1073 5
            case 'raw':
1074
                $out->lines[] = $prop[1];
1075
                break;
1076 5
            case "directive":
1077 1
                list(, $name, $value) = $prop;
1078 1
                $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)) . ';';
1079 1
                break;
1080 4
            case "comment":
1081 1
                $out->lines[] = $prop[1];
1082 1
                break;
1083 3
            case "import":
1084 3
                list(, $importPath, $importId) = $prop;
1085 3
                $importPath = $this->reduce($importPath);
1086
1087 3
                $result = $this->tryImport($importPath, $block, $out);
1088
1089 3
                $this->env->addImports($importId, $result === false ?
1090 2
                    [false, "@import " . $this->compileValue($importPath) . ";"] :
1091 3
                    $result);
1092
1093 3
                break;
1094 3
            case "import_mixin":
1095 3
                list(, $importId) = $prop;
1096 3
                $import = $this->env->getImports($importId);
1097 3
                if ($import[0] === false) {
1098 3
                    if (isset($import[1])) {
1099 3
                        $out->lines[] = $import[1];
1100
                    }
1101
                } else {
1102 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...
1103 2
                    $this->compileImportedProps($bottom, $block, $out, $importDir);
1104
                }
1105
1106 3
                break;
1107
            default:
1108
                $block->parser->throwError("unknown op: {$prop[0]}\n", $block->count);
1109
        }
1110 38
    }
1111
1112
1113
    /**
1114
     * Compiles a primitive value into a CSS property value.
1115
     *
1116
     * Values in lessphp are typed by being wrapped in arrays, their format is
1117
     * typically:
1118
     *
1119
     *     array(type, contents [, additional_contents]*)
1120
     *
1121
     * The input is expected to be reduced. This function will not work on
1122
     * things like expressions and variables.
1123
     *
1124
     * @param array $value
1125
     * @param array $options
1126
     *
1127
     * @return string
1128
     * @throws GeneralException
1129
     */
1130 53
    public function compileValue(array $value, array $options = [])
1131
    {
1132
        try {
1133 53
            if (!isset($value[0])) {
1134
                throw new GeneralException('Missing value type');
1135
            }
1136
1137 53
            $options = array_replace([
1138 53
                'numberPrecision' => $this->numberPrecision,
1139 53
                'compressColors'  => ($this->formatter ? $this->formatter->getCompressColors() : false),
1140
            ], $options);
1141
1142 53
            $valueClass = \LesserPhp\Compiler\Value\AbstractValue::factory($this, $this->coerce, $options, $value);
1143
1144 52
            return $valueClass->getCompiled();
1145 1
        } catch (\UnexpectedValueException $e) {
1146 1
            throw new GeneralException($e->getMessage());
1147
        }
1148
    }
1149
1150
    /**
1151
     * Helper function to get arguments for color manipulation functions.
1152
     * takes a list that contains a color like thing and a percentage
1153
     *
1154
     * @param array $args
1155
     *
1156
     * @return array
1157
     */
1158 2
    public function colorArgs(array $args)
1159
    {
1160 2
        if ($args[0] !== 'list' || count($args[2]) < 2) {
1161 1
            return [['color', 0, 0, 0], 0];
1162
        }
1163 2
        list($color, $delta) = $args[2];
1164 2
        $color = $this->assertions->assertColor($color);
1165 2
        $delta = (float) $delta[1];
1166
1167 2
        return [$color, $delta];
1168
    }
1169
1170
    /**
1171
     * Convert the rgb, rgba, hsl color literals of function type
1172
     * as returned by the parser into values of color type.
1173
     *
1174
     * @param array $func
1175
     *
1176
     * @return bool|mixed
1177
     */
1178 24
    protected function funcToColor(array $func)
1179
    {
1180 24
        $fname = $func[1];
1181 24
        if ($func[2][0] !== 'list') {
1182 6
            return false;
1183
        } // need a list of arguments
1184
        /** @var array $rawComponents */
1185 24
        $rawComponents = $func[2][2];
1186
1187 24
        if ($fname === 'hsl' || $fname === 'hsla') {
1188 1
            $hsl = ['hsl'];
1189 1
            $i = 0;
1190 1
            foreach ($rawComponents as $c) {
1191 1
                $val = $this->reduce($c);
1192 1
                $val = isset($val[1]) ? (float) $val[1] : 0;
1193
1194 1
                if ($i === 0) {
1195 1
                    $clamp = 360;
1196 1
                } elseif ($i < 3) {
1197 1
                    $clamp = 100;
1198
                } else {
1199 1
                    $clamp = 1;
1200
                }
1201
1202 1
                $hsl[] = $this->converter->clamp($val, $clamp);
1203 1
                $i++;
1204
            }
1205
1206 1
            while (count($hsl) < 4) {
1207
                $hsl[] = 0;
1208
            }
1209
1210 1
            return $this->converter->toRGB($hsl);
1211 24
        } elseif ($fname === 'rgb' || $fname === 'rgba') {
1212 4
            $components = [];
1213 4
            $i = 1;
1214 4
            foreach ($rawComponents as $c) {
1215 4
                $c = $this->reduce($c);
1216 4
                if ($i < 4) {
1217 4
                    if ($c[0] === "number" && $c[2] === "%") {
1218 1
                        $components[] = 255 * ($c[1] / 100);
1219
                    } else {
1220 4
                        $components[] = (float) $c[1];
1221
                    }
1222 4
                } elseif ($i === 4) {
1223 4
                    if ($c[0] === "number" && $c[2] === "%") {
1224
                        $components[] = 1.0 * ($c[1] / 100);
1225
                    } else {
1226 4
                        $components[] = (float) $c[1];
1227
                    }
1228
                } else {
1229
                    break;
1230
                }
1231
1232 4
                $i++;
1233
            }
1234 4
            while (count($components) < 3) {
1235
                $components[] = 0;
1236
            }
1237 4
            array_unshift($components, 'color');
1238
1239 4
            return $this->fixColor($components);
1240
        }
1241
1242 23
        return false;
1243
    }
1244
1245
    /**
1246
     * @param array $value
1247
     * @param bool  $forExpression
1248
     *
1249
     * @return array|bool|mixed|null // <!-- dafuq?
1250
     */
1251 48
    public function reduce(array $value, $forExpression = false)
1252
    {
1253 48
        switch ($value[0]) {
1254 48
            case "interpolate":
1255 7
                $reduced = $this->reduce($value[1]);
1256 7
                $var = $this->compileValue($reduced);
1257 7
                $res = $this->reduce(["variable", $this->vPrefix . $var]);
1258
1259 7
                if ($res[0] === "raw_color") {
1260 1
                    $res = $this->coerce->coerceColor($res);
1261
                }
1262
1263 7
                if (empty($value[2])) {
1264 6
                    $res = $this->functions->e($res);
1265
                }
1266
1267 7
                return $res;
1268 48
            case "variable":
1269 28
                $key = $value[1];
1270 28
                if (is_array($key)) {
1271 1
                    $key = $this->reduce($key);
1272 1
                    $key = $this->vPrefix . $this->compileValue($this->functions->e($key));
1273
                }
1274
1275 28
                $seen =& $this->env->seenNames;
1276
1277 28
                if (!empty($seen[$key])) {
1278
                    $this->throwError("infinite loop detected: $key");
1279
                }
1280
1281 28
                $seen[$key] = true;
1282 28
                $out = $this->reduce($this->get($key));
1283 27
                $seen[$key] = false;
1284
1285 27
                return $out;
1286 47
            case "list":
1287 29
                foreach ($value[2] as &$item) {
1288 26
                    $item = $this->reduce($item, $forExpression);
1289
                }
1290
1291 29
                return $value;
1292 47
            case "expression":
1293 19
                return $this->evaluate($value);
1294 47
            case "string":
1295 26
                foreach ($value[2] as &$part) {
1296 26
                    if (is_array($part)) {
1297 11
                        $strip = $part[0] === "variable";
1298 11
                        $part = $this->reduce($part);
1299 11
                        if ($strip) {
1300 26
                            $part = $this->functions->e($part);
1301
                        }
1302
                    }
1303
                }
1304
1305 26
                return $value;
1306 45
            case "escape":
1307 4
                list(, $inner) = $value;
1308
1309 4
                return $this->functions->e($this->reduce($inner));
1310 45
            case "function":
1311 24
                $color = $this->funcToColor($value);
1312 24
                if ($color) {
1313 4
                    return $color;
1314
                }
1315
1316 23
                list(, $name, $args) = $value;
1317 23
                if ($name === "%") {
1318 1
                    $name = "_sprintf";
1319
                }
1320
1321
                // user functions
1322 23
                $f = null;
1323 23
                if (isset($this->libFunctions[$name]) && is_callable($this->libFunctions[$name])) {
1324 1
                    $f = $this->libFunctions[$name];
1325
                }
1326
1327 23
                $func = str_replace('-', '_', $name);
1328
1329 23
                if ($f !== null || method_exists($this->functions, $func)) {
1330 14
                    if ($args[0] === 'list') {
1331 14
                        $args = self::compressList($args[2], $args[1]);
1332
                    }
1333
1334 14
                    if ($f !== null) {
1335 1
                        $ret = $f($this->reduce($args, true), $this);
1336
                    } else {
1337 13
                        $ret = $this->functions->$func($this->reduce($args, true), $this);
1338
                    }
1339 9
                    if ($ret === null) {
1340
                        return [
1341 2
                            "string",
1342 2
                            "",
1343
                            [
1344 2
                                $name,
1345 2
                                "(",
1346 2
                                $args,
1347 2
                                ")",
1348
                            ],
1349
                        ];
1350
                    }
1351
1352
                    // convert to a typed value if the result is a php primitive
1353 8
                    if (is_numeric($ret)) {
1354 3
                        $ret = ['number', $ret, ""];
1355 7
                    } elseif (!is_array($ret)) {
1356 2
                        $ret = ['keyword', $ret];
1357
                    }
1358
1359 8
                    return $ret;
1360
                }
1361
1362
                // plain function, reduce args
1363 10
                $value[2] = $this->reduce($value[2]);
1364
1365 10
                return $value;
1366 40
            case "unary":
1367 6
                list(, $op, $exp) = $value;
1368 6
                $exp = $this->reduce($exp);
1369
1370 6
                if ($exp[0] === "number") {
1371
                    switch ($op) {
1372 6
                        case "+":
1373
                            return $exp;
1374 6
                        case "-":
1375 6
                            $exp[1] *= -1;
1376
1377 6
                            return $exp;
1378
                    }
1379
                }
1380
1381
                return ["string", "", [$op, $exp]];
1382
        }
1383
1384 40
        if ($forExpression) {
1385 22
            switch ($value[0]) {
1386 22
                case "keyword":
1387 5
                    $color = $this->coerce->coerceColor($value);
1388 5
                    if ($color !== null) {
1389 2
                        return $color;
1390
                    }
1391 5
                    break;
1392 22
                case "raw_color":
1393 6
                    return $this->coerce->coerceColor($value);
1394
            }
1395
        }
1396
1397 40
        return $value;
1398
    }
1399
1400
    /**
1401
     * turn list of length 1 into value type
1402
     *
1403
     * @param array $value
1404
     *
1405
     * @return array
1406
     */
1407 2
    protected function flattenList(array $value)
1408
    {
1409 2
        if ($value[0] === 'list' && count($value[2]) === 1) {
1410 2
            return $this->flattenList($value[2][0]);
1411
        }
1412
1413 2
        return $value;
1414
    }
1415
1416
    /**
1417
     * evaluate an expression
1418
     *
1419
     * @param array $exp
1420
     *
1421
     * @return array
1422
     */
1423 19
    protected function evaluate($exp)
1424
    {
1425 19
        list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
1426
1427 19
        $left = $this->reduce($left, true);
1428 19
        $right = $this->reduce($right, true);
1429
1430 19
        $leftColor = $this->coerce->coerceColor($left);
1431 19
        if ($leftColor !== null) {
1432 5
            $left = $leftColor;
1433
        }
1434
1435 19
        $rightColor = $this->coerce->coerceColor($right);
1436 19
        if ($rightColor !== null) {
1437 5
            $right = $rightColor;
1438
        }
1439
1440 19
        $ltype = $left[0];
1441 19
        $rtype = $right[0];
1442
1443
        // operators that work on all types
1444 19
        if ($op === "and") {
1445
            return $this->functions->toBool($left == self::$TRUE && $right == self::$TRUE);
1446
        }
1447
1448 19
        if ($op === "=") {
1449 1
            return $this->functions->toBool($this->equals($left, $right));
1450
        }
1451
1452 19
        $str = $this->stringConcatenate($left, $right);
1453 19
        if ($op === "+" && $str !== null) {
1454 2
            return $str;
1455
        }
1456
1457
        // type based operators
1458 17
        $fname = "op_${ltype}_${rtype}";
1459 17
        if (is_callable([$this, $fname])) {
1460 17
            $out = $this->$fname($op, $left, $right);
1461 17
            if ($out !== null) {
1462 17
                return $out;
1463
            }
1464
        }
1465
1466
        // make the expression look it did before being parsed
1467 1
        $paddedOp = $op;
1468 1
        if ($whiteBefore) {
1469 1
            $paddedOp = " " . $paddedOp;
1470
        }
1471 1
        if ($whiteAfter) {
1472 1
            $paddedOp .= " ";
1473
        }
1474
1475 1
        return ["string", "", [$left, $paddedOp, $right]];
1476
    }
1477
1478
    /**
1479
     * @param array $left
1480
     * @param array $right
1481
     *
1482
     * @return array|null
1483
     */
1484
    protected function stringConcatenate(array $left, array $right)
1485
    {
1486 19
        $strLeft = $this->coerce->coerceString($left);
1487 19
        if ($strLeft !== null) {
1488 2
            if ($right[0] === "string") {
1489 2
                $right[1] = "";
1490
            }
1491 2
            $strLeft[2][] = $right;
1492
1493 2
            return $strLeft;
1494
        }
1495
1496 18
        $strRight = $this->coerce->coerceString($right);
1497 18
        if ($strRight !== null) {
1498 1
            array_unshift($strRight[2], $left);
1499
1500 1
            return $strRight;
1501
        }
1502
1503 17
        return null;
1504
    }
1505
1506
1507
    /**
1508
     * make sure a color's components don't go out of bounds
1509
     *
1510
     * @param array $c
1511
     *
1512
     * @return mixed
1513
     */
1514
    public function fixColor(array $c)
1515
    {
1516 7
        foreach (range(1, 3) as $i) {
1517 7
            if ($c[$i] < 0) {
1518
                $c[$i] = 0;
1519
            }
1520 7
            if ($c[$i] > 255) {
1521 7
                $c[$i] = 255;
1522
            }
1523
        }
1524
1525 7
        return $c;
1526
    }
1527
1528
    /**
1529
     * @param string $op
1530
     * @param array  $lft
1531
     * @param array  $rgt
1532
     *
1533
     * @return array|null
1534
     * @throws \LesserPhp\Exception\GeneralException
1535
     */
1536
    protected function op_number_color($op, array $lft, array $rgt)
1537
    {
1538 1
        if ($op === '+' || $op === '*') {
1539 1
            return $this->op_color_number($op, $rgt, $lft);
1540
        }
1541
1542 1
        return null;
1543
    }
1544
1545
    /**
1546
     * @param string $op
1547
     * @param array  $lft
1548
     * @param array  $rgt
1549
     *
1550
     * @return array
1551
     * @throws \LesserPhp\Exception\GeneralException
1552
     */
1553
    protected function op_color_number($op, array $lft, array $rgt)
1554
    {
1555 2
        if ($rgt[0] === '%') {
1556
            $rgt[1] /= 100;
1557
        }
1558
1559 2
        return $this->op_color_color(
1560
            $op,
1561
            $lft,
1562 2
            array_fill(1, count($lft) - 1, $rgt[1])
1563
        );
1564
    }
1565
1566
    /**
1567
     * @param string $op
1568
     * @param        array
1569
     * $left
1570
     * @param array  $right
1571
     *
1572
     * @return array
1573
     * @throws \LesserPhp\Exception\GeneralException
1574
     */
1575
    protected function op_color_color($op, array $left, array $right)
1576
    {
1577 5
        $out = ['color'];
1578 5
        $max = count($left) > count($right) ? count($left) : count($right);
1579 5
        foreach (range(1, $max - 1) as $i) {
1580 5
            $lval = isset($left[$i]) ? $left[$i] : 0;
1581 5
            $rval = isset($right[$i]) ? $right[$i] : 0;
1582
            switch ($op) {
1583 5
                case '+':
1584 5
                    $out[] = $lval + $rval;
1585 5
                    break;
1586 1
                case '-':
1587 1
                    $out[] = $lval - $rval;
1588 1
                    break;
1589 1
                case '*':
1590 1
                    $out[] = $lval * $rval;
1591 1
                    break;
1592 1
                case '%':
1593 1
                    $out[] = $lval % $rval;
1594 1
                    break;
1595 1
                case '/':
1596 1
                    if ($rval == 0) {
1597
                        throw new GeneralException("evaluate error: can't divide by zero");
1598
                    }
1599 1
                    $out[] = $lval / $rval;
1600 1
                    break;
1601
                default:
1602 5
                    throw new GeneralException('evaluate error: color op number failed on op ' . $op);
1603
            }
1604
        }
1605
1606 5
        return $this->fixColor($out);
1607
    }
1608
1609
    /**
1610
     * operator on two numbers
1611
     *
1612
     * @param string $op
1613
     * @param array  $left
1614
     * @param array  $right
1615
     *
1616
     * @return array
1617
     * @throws \LesserPhp\Exception\GeneralException
1618
     */
1619
    protected function op_number_number($op, $left, $right)
1620
    {
1621 15
        $unit = empty($left[2]) ? $right[2] : $left[2];
1622
1623 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...
1624
        switch ($op) {
1625 15
            case '+':
1626 9
                $value = $left[1] + $right[1];
1627 9
                break;
1628 12
            case '*':
1629 8
                $value = $left[1] * $right[1];
1630 8
                break;
1631 10
            case '-':
1632 6
                $value = $left[1] - $right[1];
1633 6
                break;
1634 9
            case '%':
1635 1
                $value = $left[1] % $right[1];
1636 1
                break;
1637 8
            case '/':
1638 4
                if ($right[1] == 0) {
1639
                    throw new GeneralException('parse error: divide by zero');
1640
                }
1641 4
                $value = $left[1] / $right[1];
1642 4
                break;
1643 5
            case '<':
1644 1
                return $this->functions->toBool($left[1] < $right[1]);
1645 5
            case '>':
1646 3
                return $this->functions->toBool($left[1] > $right[1]);
1647 3
            case '>=':
1648 1
                return $this->functions->toBool($left[1] >= $right[1]);
1649 2
            case '=<':
1650 2
                return $this->functions->toBool($left[1] <= $right[1]);
1651
            default:
1652
                throw new GeneralException('parse error: unknown number operator: ' . $op);
1653
        }
1654
1655 13
        return ['number', $value, $unit];
1656
    }
1657
1658
    /**
1659
     * @param      $type
1660
     * @param null $selectors
1661
     *
1662
     * @return \stdClass
1663
     */
1664
    protected function makeOutputBlock($type, $selectors = null)
1665
    {
1666 49
        $b = new \stdClass();
1667 49
        $b->lines = [];
1668 49
        $b->children = [];
1669 49
        $b->selectors = $selectors;
1670 49
        $b->type = $type;
1671 49
        $b->parent = $this->scope;
1672
1673 49
        return $b;
1674
    }
1675
1676
    /**
1677
     * @param \LesserPhp\NodeEnv $parent
1678
     * @param Block|null $block
1679
     *
1680
     * @return \LesserPhp\NodeEnv
1681
     */
1682
    protected function pushEnv($parent, Block $block = null)
1683
    {
1684 49
        $e = new NodeEnv();
1685 49
        $e->setParent($parent);
1686 49
        $e->setBlock($block);
0 ignored issues
show
Documentation introduced by
$block is of type null|object<LesserPhp\Block>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1687 49
        $e->setStore([]);
1688
1689 49
        $this->env = $e;
1690
1691 49
        return $e;
1692
    }
1693
1694
    /**
1695
     * pop something off the stack
1696
     *
1697
     * @return \LesserPhp\NodeEnv
1698
     */
1699
    protected function popEnv()
1700
    {
1701 40
        $old = $this->env;
1702 40
        $this->env = $this->env->getParent();
1703
1704 40
        return $old;
1705
    }
1706
1707
    /**
1708
     * set something in the current env
1709
     *
1710
     * @param $name
1711
     * @param $value
1712
     */
1713
    protected function set($name, $value)
1714
    {
1715 27
        $this->env->addStore($name, $value);
1716 27
    }
1717
1718
    /**
1719
     * get the highest occurrence entry for a name
1720
     *
1721
     * @param $name
1722
     *
1723
     * @return array
1724
     * @throws \LesserPhp\Exception\GeneralException
1725
     */
1726
    protected function get($name)
1727
    {
1728 28
        $current = $this->env;
1729
1730
        // track scope to evaluate
1731 28
        $scopeSecondary = [];
1732
1733 28
        $isArguments = $name === $this->vPrefix . 'arguments';
1734 28
        while ($current) {
1735 28
            if ($isArguments && count($current->getArguments()) > 0) {
1736 3
                return ['list', ' ', $current->getArguments()];
1737
            }
1738
1739 28
            if (isset($current->getStore()[$name])) {
1740 27
                return $current->getStore()[$name];
1741
            }
1742
            // has secondary scope?
1743 19
            if (isset($current->storeParent)) {
1744
                $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...
1745
            }
1746
1747 19
            if ($current->getParent() !== null) {
1748 19
                $current = $current->getParent();
1749
            } else {
1750 1
                $current = null;
1751
            }
1752
        }
1753
1754 1
        while (count($scopeSecondary)) {
1755
            // pop one off
1756
            $current = array_shift($scopeSecondary);
1757
            while ($current) {
1758
                if ($isArguments && isset($current->arguments)) {
1759
                    return ['list', ' ', $current->arguments];
1760
                }
1761
1762
                if (isset($current->store[$name])) {
1763
                    return $current->store[$name];
1764
                }
1765
1766
                // has secondary scope?
1767
                if (isset($current->storeParent)) {
1768
                    $scopeSecondary[] = $current->storeParent;
1769
                }
1770
1771
                if (isset($current->parent)) {
1772
                    $current = $current->parent;
1773
                } else {
1774
                    $current = null;
1775
                }
1776
            }
1777
        }
1778
1779 1
        throw new GeneralException("variable $name is undefined");
1780
    }
1781
1782
    /**
1783
     * inject array of unparsed strings into environment as variables
1784
     *
1785
     * @param string[] $args
1786
     *
1787
     * @throws \LesserPhp\Exception\GeneralException
1788
     */
1789
    protected function injectVariables(array $args)
1790
    {
1791 1
        $this->pushEnv($this->env);
1792 1
        $parser = new Parser($this, __METHOD__);
1793 1
        foreach ($args as $name => $strValue) {
1794 1
            if ($name{0} !== '@') {
1795 1
                $name = '@' . $name;
1796
            }
1797 1
            $parser->count = 0;
1798 1
            $parser->buffer = (string) $strValue;
1799 1
            if (!$parser->propertyValue($value)) {
1800
                throw new GeneralException("failed to parse passed in variable $name: $strValue");
1801
            }
1802
1803 1
            $this->set($name, $value);
1804
        }
1805 1
    }
1806
1807
    /**
1808
     * @param string $string
1809
     * @param string $name
1810
     *
1811
     * @return string
1812
     * @throws \LesserPhp\Exception\GeneralException
1813
     */
1814
    public function compile($string, $name = null)
1815
    {
1816 49
        $locale = setlocale(LC_NUMERIC, 0);
1817 49
        setlocale(LC_NUMERIC, 'C');
1818
1819 49
        $this->parser = $this->makeParser($name);
1820 49
        $root = $this->parser->parse($string);
1821
1822 49
        $this->env = null;
1823 49
        $this->scope = null;
1824 49
        $this->allParsedFiles = [];
1825
1826 49
        $this->formatter = $this->newFormatter();
1827
1828 49
        if (!empty($this->registeredVars)) {
1829 1
            $this->injectVariables($this->registeredVars);
1830
        }
1831
1832 49
        $this->sourceParser = $this->parser; // used for error messages
1833 49
        $this->compileBlock($root);
1834
1835 38
        ob_start();
1836 38
        $this->formatter->block($this->scope);
1837 38
        $out = ob_get_clean();
1838 38
        setlocale(LC_NUMERIC, $locale);
1839
1840 38
        return $out;
1841
    }
1842
1843
    /**
1844
     * @param string $fname
1845
     * @param string $outFname
1846
     *
1847
     * @return int|string
1848
     * @throws \LesserPhp\Exception\GeneralException
1849
     */
1850
    public function compileFile($fname, $outFname = null)
1851
    {
1852 1
        if (!is_readable($fname)) {
1853
            throw new GeneralException('load error: failed to find ' . $fname);
1854
        }
1855
1856 1
        $pi = pathinfo($fname);
1857
1858 1
        $oldImport = $this->importDirs;
1859
1860 1
        $this->importDirs[] = $pi['dirname'] . '/';
1861
1862 1
        $this->addParsedFile($fname);
1863
1864 1
        $out = $this->compile(file_get_contents($fname), $fname);
1865
1866 1
        $this->importDirs = $oldImport;
1867
1868 1
        if ($outFname !== null) {
1869
            return file_put_contents($outFname, $out);
1870
        }
1871
1872 1
        return $out;
1873
    }
1874
1875
    /**
1876
     * Based on explicit input/output files does a full change check on cache before compiling.
1877
     *
1878
     * @param string  $in
1879
     * @param string  $out
1880
     * @param boolean $force
1881
     *
1882
     * @return string Compiled CSS results
1883
     * @throws GeneralException
1884
     */
1885
    public function checkedCachedCompile($in, $out, $force = false)
1886
    {
1887 1
        if (!is_file($in) || !is_readable($in)) {
1888
            throw new GeneralException('Invalid or unreadable input file specified.');
1889
        }
1890 1
        if (is_dir($out) || !is_writable(file_exists($out) ? $out : dirname($out))) {
1891
            throw new GeneralException('Invalid or unwritable output file specified.');
1892
        }
1893
1894 1
        $outMeta = $out . '.meta';
1895 1
        $metadata = null;
1896 1
        if (!$force && is_file($outMeta)) {
1897
            $metadata = unserialize(file_get_contents($outMeta));
1898
        }
1899
1900 1
        $output = $this->cachedCompile($metadata ?: $in);
1901
1902 1
        if (!$metadata || $metadata['updated'] != $output['updated']) {
1903 1
            $css = $output['compiled'];
1904 1
            unset($output['compiled']);
1905 1
            file_put_contents($out, $css);
1906 1
            file_put_contents($outMeta, serialize($output));
1907
        } else {
1908
            $css = file_get_contents($out);
1909
        }
1910
1911 1
        return $css;
1912
    }
1913
1914
    /**
1915
     * compile only if changed input has changed or output doesn't exist
1916
     *
1917
     * @param string $in
1918
     * @param string $out
1919
     *
1920
     * @return bool
1921
     * @throws \LesserPhp\Exception\GeneralException
1922
     */
1923
    public function checkedCompile($in, $out)
1924
    {
1925
        if (!is_file($out) || filemtime($in) > filemtime($out)) {
1926
            $this->compileFile($in, $out);
1927
1928
            return true;
1929
        }
1930
1931
        return false;
1932
    }
1933
1934
    /**
1935
     * Execute lessphp on a .less file or a lessphp cache structure
1936
     *
1937
     * The lessphp cache structure contains information about a specific
1938
     * less file having been parsed. It can be used as a hint for future
1939
     * calls to determine whether or not a rebuild is required.
1940
     *
1941
     * The cache structure contains two important keys that may be used
1942
     * externally:
1943
     *
1944
     * compiled: The final compiled CSS
1945
     * updated: The time (in seconds) the CSS was last compiled
1946
     *
1947
     * The cache structure is a plain-ol' PHP associative array and can
1948
     * be serialized and unserialized without a hitch.
1949
     *
1950
     * @param mixed $in    Input
1951
     * @param bool  $force Force rebuild?
1952
     *
1953
     * @return array lessphp cache structure
1954
     * @throws \LesserPhp\Exception\GeneralException
1955
     */
1956
    public function cachedCompile($in, $force = false)
1957
    {
1958
        // assume no root
1959 1
        $root = null;
1960
1961 1
        if (is_string($in)) {
1962 1
            $root = $in;
1963
        } elseif (is_array($in) && isset($in['root'])) {
1964
            if ($force || !isset($in['files'])) {
1965
                // If we are forcing a recompile or if for some reason the
1966
                // structure does not contain any file information we should
1967
                // specify the root to trigger a rebuild.
1968
                $root = $in['root'];
1969
            } elseif (isset($in['files']) && is_array($in['files'])) {
1970
                foreach ($in['files'] as $fname => $ftime) {
1971
                    if (!file_exists($fname) || filemtime($fname) > $ftime) {
1972
                        // One of the files we knew about previously has changed
1973
                        // so we should look at our incoming root again.
1974
                        $root = $in['root'];
1975
                        break;
1976
                    }
1977
                }
1978
            }
1979
        } else {
1980
            // TODO: Throw an exception? We got neither a string nor something
1981
            // that looks like a compatible lessphp cache structure.
1982
            return null;
1983
        }
1984
1985 1
        if ($root !== null) {
1986
            // If we have a root value which means we should rebuild.
1987 1
            $out = [];
1988 1
            $out['root'] = $root;
1989 1
            $out['compiled'] = $this->compileFile($root);
1990 1
            $out['files'] = $this->allParsedFiles;
1991 1
            $out['updated'] = time();
1992
1993 1
            return $out;
1994
        } else {
1995
            // No changes, pass back the structure
1996
            // we were given initially.
1997
            return $in;
1998
        }
1999
    }
2000
2001
    /**
2002
     * parse and compile buffer
2003
     * This is deprecated
2004
     *
2005
     * @param null $str
2006
     * @param null $initialVariables
2007
     *
2008
     * @return int|string
2009
     * @throws \LesserPhp\Exception\GeneralException
2010
     * @deprecated
2011
     */
2012
    public function parse($str = null, $initialVariables = null)
2013
    {
2014 37
        if (is_array($str)) {
2015
            $initialVariables = $str;
2016
            $str = null;
2017
        }
2018
2019 37
        $oldVars = $this->registeredVars;
2020 37
        if ($initialVariables !== null) {
2021 1
            $this->setVariables($initialVariables);
2022
        }
2023
2024 37
        if ($str === null) {
2025
            throw new GeneralException('nothing to parse');
2026
        } else {
2027 37
            $out = $this->compile($str);
2028
        }
2029
2030 37
        $this->registeredVars = $oldVars;
2031
2032 37
        return $out;
2033
    }
2034
2035
    /**
2036
     * @param string $name
2037
     *
2038
     * @return \LesserPhp\Parser
2039
     */
2040
    protected function makeParser($name)
2041
    {
2042 49
        $parser = new Parser($this, $name);
2043 49
        $parser->setWriteComments($this->preserveComments);
2044
2045 49
        return $parser;
2046
    }
2047
2048
    /**
2049
     * @param string $name
2050
     */
2051
    public function setFormatter($name)
2052
    {
2053 1
        $this->formatterName = $name;
2054 1
    }
2055
2056
    public function setFormatterClass($formatter)
2057
    {
2058 14
        $this->formatter = $formatter;
2059 14
    }
2060
2061
    /**
2062
     * @return \LesserPhp\Formatter\FormatterInterface
2063
     */
2064
    protected function newFormatter()
2065
    {
2066 49
        $className = 'Lessjs';
2067 49
        if (!empty($this->formatterName)) {
2068 1
            if (!is_string($this->formatterName)) {
2069
                return $this->formatterName;
2070
            }
2071 1
            $className = $this->formatterName;
2072
        }
2073
2074 49
        $className = '\LesserPhp\Formatter\\' . $className;
2075
2076 49
        return new $className;
2077
    }
2078
2079
    /**
2080
     * @param bool $preserve
2081
     */
2082
    public function setPreserveComments($preserve)
2083
    {
2084 1
        $this->preserveComments = $preserve;
2085 1
    }
2086
2087
    /**
2088
     * @param string   $name
2089
     * @param callable $func
2090
     */
2091
    public function registerFunction($name, callable $func)
2092
    {
2093 1
        $this->libFunctions[$name] = $func;
2094 1
    }
2095
2096
    /**
2097
     * @param string $name
2098
     */
2099
    public function unregisterFunction($name)
2100
    {
2101 1
        unset($this->libFunctions[$name]);
2102 1
    }
2103
2104
    /**
2105
     * @param array $variables
2106
     */
2107
    public function setVariables(array $variables)
2108
    {
2109 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...
2110 1
    }
2111
2112
    /**
2113
     * @param $name
2114
     */
2115
    public function unsetVariable($name)
2116
    {
2117
        unset($this->registeredVars[$name]);
2118
    }
2119
2120
    /**
2121
     * @param string[] $dirs
2122
     */
2123
    public function setImportDirs(array $dirs)
2124
    {
2125 38
        $this->importDirs = $dirs;
2126 38
    }
2127
2128
    /**
2129
     * @param string $dir
2130
     */
2131
    public function addImportDir($dir)
2132
    {
2133
        $this->importDirs[] = $dir;
2134
    }
2135
2136
    /**
2137
     * @return string[]
2138
     */
2139
    public function getImportDirs()
2140
    {
2141 4
        return $this->importDirs;
2142
    }
2143
2144
    /**
2145
     * @param string $file
2146
     */
2147
    public function addParsedFile($file)
2148
    {
2149 2
        $this->allParsedFiles[realpath($file)] = filemtime($file);
2150 2
    }
2151
2152
    /**
2153
     * Uses the current value of $this->count to show line and line number
2154
     *
2155
     * @param string $msg
2156
     *
2157
     * @throws GeneralException
2158
     */
2159
    public function throwError($msg = null)
2160
    {
2161
        if ($this->sourceLoc >= 0) {
2162
            $this->sourceParser->throwError($msg, $this->sourceLoc);
2163
        }
2164
        throw new GeneralException($msg);
2165
    }
2166
2167
    /**
2168
     * compile file $in to file $out if $in is newer than $out
2169
     * returns true when it compiles, false otherwise
2170
     *
2171
     * @param                          $in
2172
     * @param                          $out
2173
     * @param \LesserPhp\Compiler|null $less
2174
     *
2175
     * @return bool
2176
     * @throws \LesserPhp\Exception\GeneralException
2177
     */
2178
    public static function ccompile($in, $out, Compiler $less = null)
2179
    {
2180
        if ($less === null) {
2181
            $less = new self;
2182
        }
2183
2184
        return $less->checkedCompile($in, $out);
2185
    }
2186
2187
    /**
2188
     * @param                          $in
2189
     * @param bool                     $force
2190
     * @param \LesserPhp\Compiler|null $less
2191
     *
2192
     * @return array
2193
     * @throws \LesserPhp\Exception\GeneralException
2194
     */
2195
    public static function cexecute($in, $force = false, Compiler $less = null)
2196
    {
2197
        if ($less === null) {
2198
            $less = new self;
2199
        }
2200
2201
        return $less->cachedCompile($in, $force);
2202
    }
2203
2204
    /**
2205
     * prefix of abstract properties
2206
     *
2207
     * @return string
2208
     */
2209
    public function getVPrefix()
2210
    {
2211 49
        return $this->vPrefix;
2212
    }
2213
2214
    /**
2215
     * prefix of abstract blocks
2216
     *
2217
     * @return string
2218
     */
2219
    public function getMPrefix()
2220
    {
2221 46
        return $this->mPrefix;
2222
    }
2223
2224
    /**
2225
     * @return string
2226
     */
2227
    public function getParentSelector()
2228
    {
2229 3
        return $this->parentSelector;
2230
    }
2231
2232
    /**
2233
     * @param int $numberPresicion
2234
     */
2235
    protected function setNumberPrecision($numberPresicion = null)
2236
    {
2237
        $this->numberPrecision = $numberPresicion;
2238
    }
2239
2240
    /**
2241
     * @return \LesserPhp\Library\Coerce
2242
     */
2243
    protected function getCoerce()
2244
    {
2245
        return $this->coerce;
2246
    }
2247
2248
    public function setImportDisabled()
2249
    {
2250 1
        $this->importDisabled = true;
2251 1
    }
2252
2253
    /**
2254
     * @return bool
2255
     */
2256
    public function isImportDisabled()
2257
    {
2258 3
        return $this->importDisabled;
2259
    }
2260
}
2261