Compiler::reduce()   F
last analyzed

Complexity

Conditions 35
Paths 94

Size

Total Lines 148

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 88
CRAP Score 35.0439

Importance

Changes 0
Metric Value
dl 0
loc 148
ccs 88
cts 91
cp 0.967
rs 3.3333
c 0
b 0
f 0
cc 35
nc 94
nop 2
crap 35.0439

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace LesserPhp;
4
5
/**
6
 * lesserphp
7
 * https://www.maswaba.de/lesserphp
8
 *
9
 * LESS CSS compiler, adapted from http://lesscss.org
10
 *
11
 * Copyright 2013, Leaf Corcoran <[email protected]>
12
 * Copyright 2016, Marcus Schwarz <[email protected]>
13
 * Licensed under MIT or GPLv3, see LICENSE
14
 * @package LesserPhp
15
 */
16
use LesserPhp\Color\Converter;
17
use LesserPhp\Exception\GeneralException;
18
use LesserPhp\Library\Assertions;
19
use LesserPhp\Library\Coerce;
20
use LesserPhp\Library\Functions;
21
22
/**
23
 * The LESS compiler and parser.
24
 *
25
 * Converting LESS to CSS is a three stage process. The incoming file is parsed
26
 * by `lessc_parser` into a syntax tree, then it is compiled into another tree
27
 * representing the CSS structure by `lessc`. The CSS tree is fed into a
28
 * formatter, like `lessc_formatter` which then outputs CSS as a string.
29
 *
30
 * During the first compile, all values are *reduced*, which means that their
31
 * types are brought to the lowest form before being dump as strings. This
32
 * handles math equations, variable dereferences, and the like.
33
 *
34
 * The `parse` function of `lessc` is the entry point.
35
 *
36
 * In summary:
37
 *
38
 * The `lessc` class creates an instance of the parser, feeds it LESS code,
39
 * then transforms the resulting tree to a CSS tree. This class also holds the
40
 * evaluation context, such as all available mixins and variables at any given
41
 * time.
42
 *
43
 * The `lessc_parser` class is only concerned with parsing its input.
44
 *
45
 * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string,
46
 * handling things like indentation.
47
 */
48
class Compiler
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 Block     $parentBlock
208
     * @param \stdClass $out
209
     *
210
     * @return array|false
211
     * @throws \LesserPhp\Exception\GeneralException
212
     */
213 3
    protected function tryImport(array $importPath, Block $parentBlock, \stdClass $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 instanceof Property\BlockProperty) {
252 2
                $prop->getBlock()->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 Property[] $props
281
     * @param Block      $block
282
     * @param \stdClass  $out
283
     * @param string     $importDir
284
     */
285 2
    protected function compileImportedProps(array $props, Block $block, \stdClass $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, \stdClass $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 Property[] $props
488
     * @param bool       $split
489
     *
490
     * @return Property[]
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 (true) {
501 49
                case $prop instanceof Property\CommentProperty:
502 1
                    $stack[] = $prop;
503 1
                    break;
504 49
505 43
                case $prop instanceof Property\AssignProperty:
506 43
                    $stack[] = $prop;
507 23
                    if ($prop->nameHasPrefix($this->vPrefix)) {
508
                        $vars = array_merge($vars, $stack);
509 43
                    } else {
510
                        $other = array_merge($other, $stack);
511 43
                    }
512 43
                    $stack = [];
513 47
                    break;
514 3
515 3
                case $prop instanceof Property\ImportProperty:
516 3
                    $id = self::$nextImportId++;
517 3
                    $prop->setId($id);
518 3
519 3
                    $stack[] = $prop;
520 3
                    $imports = array_merge($imports, $stack);
521
                    $other[] = Property::factoryFromOldFormat(["import_mixin", $id]);
522 46
                    $stack   = [];
523 46
                    break;
524 46
525 49
                default:
526
                    $stack[] = $prop;
527
                    $other   = array_merge($other, $stack);
528 49
                    $stack   = [];
529
                    break;
530 49
            }
531 2
        }
532
533 49
        $other = array_merge($other, $stack);
534
535
        if ($split) {
536
            return [array_merge($vars, $imports, $vars), $other];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array(array_merge...ports, $vars), $other); (array[]) is incompatible with the return type documented by LesserPhp\Compiler::sortProps of type LesserPhp\Property[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
537
        } else {
538
            return array_merge($vars, $imports, $vars, $other);
539
        }
540
    }
541
542 3
    /**
543
     * @param array $queries
544 3
     *
545 3
     * @return string
546 3
     */
547 3
    protected function compileMediaQuery(array $queries)
548 3
    {
549 3
        $compiledQueries = [];
550 3
        foreach ($queries as $query) {
551 3
            $parts = [];
552 1
            foreach ($query as $q) {
553 1
                switch ($q[0]) {
554 1
                    case "mediaType":
555 1
                        $parts[] = implode(" ", array_slice($q, 1));
556
                        break;
557 1
                    case "mediaExp":
558
                        if (isset($q[2])) {
559 1
                            $parts[] = "($q[1]: " .
560 1
                                $this->compileValue($this->reduce($q[2])) . ")";
561 1
                        } else {
562 3
                            $parts[] = "($q[1])";
563
                        }
564
                        break;
565
                    case "variable":
566 3
                        $parts[] = $this->compileValue($this->reduce($q));
567 3
                        break;
568
                }
569
            }
570
571 3
            if (count($parts) > 0) {
572 3
                $compiledQueries[] = implode(" and ", $parts);
573
            }
574 3
        }
575
576
        $out = "@media";
577 3
        if (!empty($parts)) {
578
            $out .= " " .
579
                implode($this->formatter->getSelectorSeparator(), $compiledQueries);
580
        }
581
582
        return $out;
583
    }
584
585
    /**
586 3
     * @param \LesserPhp\NodeEnv $env
587
     * @param array              $childQueries
588 3
     *
589 3
     * @return array
590
     */
591 3
    protected function multiplyMedia(NodeEnv $env = null, array $childQueries = null)
592
    {
593
        if ($env === null ||
594
            (!empty($env->getBlock()->type) && $env->getBlock()->type !== 'media')
595 3
        ) {
596 3
            return $childQueries;
597
        }
598
599 3
        // plain old block, skip
600 3
        if (empty($env->getBlock()->type)) {
601 3
            return $this->multiplyMedia($env->getParent(), $childQueries);
602 3
        }
603
604 1
        $out = [];
605 1
        $queries = $env->getBlock()->queries;
606 1
        if ($childQueries === null) {
607
            $out = $queries;
608
        } else {
609
            foreach ($queries as $parent) {
610
                foreach ($childQueries as $child) {
611 3
                    $out[] = array_merge($parent, $child);
612
                }
613
            }
614
        }
615
616
        return $this->multiplyMedia($env->getParent(), $out);
617
    }
618
619
    /**
620 46
     * @param $tag
621
     * @param $replace
622 46
     *
623 46
     * @return int
624 46
     */
625 46
    protected function expandParentSelectors(&$tag, $replace)
626 46
    {
627
        $parts = explode("$&$", $tag);
628 46
        $count = 0;
629
        foreach ($parts as &$part) {
630 46
            $part = str_replace($this->parentSelector, $replace, $part, $c);
631
            $count += $c;
632
        }
633
        $tag = implode($this->parentSelector, $parts);
634
635
        return $count;
636 46
    }
637
638 46
    /**
639 46
     * @return array|null
640 46
     */
641 46
    protected function findClosestSelectors()
642 13
    {
643 13
        $env = $this->env;
644
        $selectors = null;
645 46
        while ($env !== null) {
646
            if ($env->getSelectors() !== null) {
647
                $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...
648 46
                break;
649
            }
650
            $env = $env->getParent();
651
        }
652
653
        return $selectors;
654
    }
655
656
657
    /**
658
     *  multiply $selectors against the nearest selectors in env
659 46
     *
660
     * @param array $selectors
661
     *
662
     * @return array
663 46
     */
664 46
    protected function multiplySelectors(array $selectors)
665
    {
666 46
        // find parent selectors
667 46
668
        $parentSelectors = $this->findClosestSelectors();
669
        if ($parentSelectors === null) {
670 46
            // kill parent reference in top level selector
671
            foreach ($selectors as &$s) {
672
                $this->expandParentSelectors($s, "");
673 13
            }
674 13
675 13
            return $selectors;
676 13
        }
677
678
        $out = [];
679 13
        foreach ($parentSelectors as $parent) {
680 4
            foreach ($selectors as $child) {
681
                $count = $this->expandParentSelectors($child, $parent);
682 13
683
                // don't prepend the parent tag if & was used
684
                if ($count > 0) {
685
                    $out[] = trim($child);
686
                } else {
687 13
                    $out[] = trim($parent . ' ' . $child);
688
                }
689
            }
690
        }
691
692
        return $out;
693
    }
694
695
    /**
696
     * reduces selector expressions
697
     *
698 46
     * @param array $selectors
699
     *
700 46
     * @return array
701
     * @throws \LesserPhp\Exception\GeneralException
702 46
     */
703 46
    protected function compileSelectors(array $selectors)
704 4
    {
705 4
        $out = [];
706
707 46
        foreach ($selectors as $s) {
708
            if (is_array($s)) {
709
                list(, $value) = $s;
710
                $out[] = trim($this->compileValue($this->reduce($value)));
711 46
            } else {
712
                $out[] = $s;
713
            }
714
        }
715
716
        return $out;
717
    }
718
719
    /**
720 4
     * @param $left
721
     * @param $right
722 4
     *
723
     * @return bool
724
     */
725
    protected function equals($left, $right)
726
    {
727
        return $left == $right;
728
    }
729
730
    /**
731
     * @param $block
732 21
     * @param $orderedArgs
733
     * @param $keywordArgs
734
     *
735
     * @return bool
736 21
     */
737 5
    protected function patternMatch($block, $orderedArgs, $keywordArgs)
738 5
    {
739 5
        // match the guards if it has them
740 5
        // any one of the groups must have all its guards pass for a match
741 5
        if (!empty($block->guards)) {
742
            $groupPassed = false;
743 5
            foreach ($block->guards as $guardGroup) {
744 5
                foreach ($guardGroup as $guard) {
745 1
                    $this->pushEnv($this->env);
746 1
                    $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
747
748
                    $negate = false;
749 5
                    if ($guard[0] === "negate") {
750 5
                        $guard = $guard[1];
751 1
                        $negate = true;
752
                    }
753
754 5
                    $passed = $this->reduce($guard) == self::$TRUE;
755
                    if ($negate) {
756 5
                        $passed = !$passed;
757 3
                    }
758
759 5
                    $this->popEnv();
760 5
761
                    if ($passed) {
762
                        $groupPassed = true;
763
                    } else {
764 5
                        $groupPassed = false;
765 5
                        break;
766
                    }
767
                }
768
769 5
                if ($groupPassed) {
770 5
                    break;
771
                }
772
            }
773
774 19
            if (!$groupPassed) {
775 12
                return false;
776
            }
777
        }
778 14
779 14
        if (empty($block->args)) {
780 2
            return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
781 2
        }
782 2
783 2
        $remainingArgs = $block->args;
784
        if ($keywordArgs) {
785
            $remainingArgs = [];
786 2
            foreach ($block->args as $arg) {
787
                if ($arg[0] === "arg" && isset($keywordArgs[$arg[1]])) {
788
                    continue;
789
                }
790 14
791
                $remainingArgs[] = $arg;
792 14
            }
793 14
        }
794 14
795 3
        $i = -1; // no args
796 2
        // try to match by arity or by argument literal
797
        foreach ($remainingArgs as $i => $arg) {
798 3
            switch ($arg[0]) {
799 14
                case "lit":
800
                    if (empty($orderedArgs[$i]) || !$this->equals($arg[1], $orderedArgs[$i])) {
801 14
                        return false;
802 3
                    }
803
                    break;
804 14
                case "arg":
805 2
                    // no arg and no default value
806 2
                    if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
807 14
                        return false;
808
                    }
809
                    break;
810
                case "rest":
811 13
                    $i--; // rest can be empty
812 2
                    break 2;
813
            }
814 13
        }
815
816
        if ($block->isVararg) {
817 13
            return true; // not having enough is handled above
818
        } else {
819
            $numMatched = $i + 1;
820
821
            // greater than because default values always match
822
            return $numMatched >= count($orderedArgs);
823
        }
824
    }
825
826
    /**
827
     * @param array $blocks
828
     * @param       $orderedArgs
829 21
     * @param       $keywordArgs
830
     * @param array $skip
831 21
     *
832 21
     * @return array|null
833
     */
834 21
    protected function patternMatchAll(array $blocks, $orderedArgs, $keywordArgs, array $skip = [])
835 1
    {
836
        $matches = null;
837
        foreach ($blocks as $block) {
838 21
            // skip seen blocks that don't have arguments
839 21
            if (isset($skip[$block->id]) && !isset($block->args)) {
840
                continue;
841
            }
842
843 21
            if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
844
                $matches[] = $block;
845
            }
846
        }
847
848
        return $matches;
849
    }
850
851
    /**
852
     * attempt to find blocks matched by path and args
853
     *
854
     * @param       $searchIn
855
     * @param array $path
856
     * @param       $orderedArgs
857 22
     * @param       $keywordArgs
858
     * @param array $seen
859 22
     *
860 5
     * @return array|null
861
     */
862 22
    protected function findBlocks($searchIn, array $path, $orderedArgs, $keywordArgs, array $seen = [])
863 1
    {
864
        if ($searchIn === null) {
865 22
            return null;
866
        }
867 22
        if (isset($seen[$searchIn->id])) {
868
            return null;
869 22
        }
870 21
        $seen[$searchIn->id] = true;
871 21
872 21
        $name = $path[0];
873 21
874
        if (isset($searchIn->children[$name])) {
875
            $blocks = $searchIn->children[$name];
876 21
            if (count($path) === 1) {
877
                $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
878
                if (!empty($matches)) {
879 3
                    // This will return all blocks that match in the closest
880 3
                    // scope that has any matching block, like lessjs
881 3
                    return $matches;
882
                }
883 3
            } else {
884
                $matches = [];
885
                foreach ($blocks as $subBlock) {
886
                    $subMatches = $this->findBlocks(
887
                        $subBlock,
888
                        array_slice($path, 1),
889 3
                        $orderedArgs,
890 3
                        $keywordArgs,
891 3
                        $seen
892
                    );
893
894
                    if ($subMatches !== null) {
895
                        foreach ($subMatches as $sm) {
896 3
                            $matches[] = $sm;
897
                        }
898
                    }
899 22
                }
900
901
                return count($matches) > 0 ? $matches : null;
902
            }
903 22
        }
904
        if ($searchIn->parent === $searchIn) {
905
            return null;
906
        }
907
908
        return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
909
    }
910
911
    /**
912
     * sets all argument names in $args to either the default value
913
     * or the one passed in through $values
914
     *
915
     * @param array $args
916 16
     * @param       $orderedValues
917
     * @param       $keywordValues
918 16
     *
919
     * @throws \LesserPhp\Exception\GeneralException
920 16
     */
921 16
    protected function zipSetArgs(array $args, $orderedValues, $keywordValues)
922 14
    {
923 14
        $assignedValues = [];
924
925 2
        $i = 0;
926 14
        foreach ($args as $a) {
927
            if ($a[0] === "arg") {
928 13
                if (isset($keywordValues[$a[1]])) {
929 13
                    // has keyword arg
930 6
                    $value = $keywordValues[$a[1]];
931
                } elseif (isset($orderedValues[$i])) {
932 6
                    // has ordered arg
933
                    $value = $orderedValues[$i];
934
                    $i++;
935
                } elseif (isset($a[2])) {
936
                    // has default value
937 14
                    $value = $a[2];
938 14
                } else {
939 14
                    throw new GeneralException('Failed to assign arg ' . $a[1]);
940
                }
941
942 14
                $value = $this->reduce($value);
943
                $this->set($a[1], $value);
944
                $assignedValues[] = $value;
945
            } else {
946
                // a lit
947 16
                $i++;
948 16
            }
949 2
        }
950 2
951
        // check for a rest
952
        $last = end($args);
953
        if ($last !== false && $last[0] === "rest") {
954 16
            $rest = array_slice($orderedValues, count($args) - 1);
955 16
            $this->set($last[1], $this->reduce(["list", " ", $rest]));
956
        }
957
958
        // wow is this the only true use of PHP's + operator for arrays?
959
        $this->env->setArguments($assignedValues + $orderedValues);
960
    }
961
962
    /**
963
     * compile a prop and update $lines or $blocks appropriately
964
     *
965
     * @param Property  $prop
966 49
     * @param Block     $block
967
     * @param \stdClass $out
968
     *
969 49
     * @throws \LesserPhp\Exception\GeneralException
970
     */
971 49
    protected function compileProp(Property $prop, Block $block, \stdClass $out)
972 49
    {
973 43
        $this->sourceLoc = ($prop->hasPos() ? $prop->getPos() : -1);
974 43
975 23
        if ($prop instanceof Property\CanCompile) {
976
            $out->lines[] = $prop->compile($this);
977 43
978
            return;
979 43
        }
980
981
        switch (true) {
982 37
            case $prop instanceof Property\AssignProperty:
983 47
                if ($prop->nameHasPrefix($this->vPrefix)) {
984 46
                    $this->set($prop->getName(), $prop->getValue());
985 46
                } else {
986 35
                    $out->lines[] = $this->formatter->property(
987 25
                        $prop->getName(),
988 25
                        $this->compileValue($this->reduce($prop->getValue()))
989 22
                    );
990
                }
991 22
992 22
                return;
993 22
994 15
            case $prop instanceof Property\BlockProperty:
995 15
                $this->compileBlock($prop->getBlock());
996 4
997 3
                return;
998
999 2
            case $prop instanceof Property\ImportProperty:
1000
                $importPath = $this->reduce($prop->getPath());
0 ignored issues
show
Documentation introduced by
$prop->getPath() is of type string, but the function expects a array.

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...
1001 4
                $result     = $this->tryImport($importPath, $block, $out);
1002
                if ($result === false) {
1003 15
                    $result = [false, "@import " . $this->compileValue($importPath) . ";"];
1004 15
                }
1005 15
                $this->env->addImports($prop->getId(), $result);
1006
1007 15
                return;
1008
1009
            case $prop instanceof Property\ImportMixinProperty:
1010
                $import = $this->env->getImports($prop->getId());
1011 22
                if ($import[0] === false) {
1012
                    if (isset($import[1])) {
1013 22
                        $out->lines[] = $import[1];
1014 5
                    }
1015
                } else {
1016
                    $bottom    = $import[1];
1017 17
                    $importDir = $import[3];
1018
                    $this->compileImportedProps($bottom, $block, $out, $importDir);
1019 8
                }
1020
1021
                return;
1022 17
1023 17
            case $prop instanceof Property\RulesetProperty:
1024
            case $prop instanceof Property\MixinProperty:
1025
                $path   = $prop->getPath();
1026
                $args   = $prop->getArgs();
1027 17
                $suffix = $prop->getSuffix();
1028 17
1029 2
                $orderedArgs = [];
1030 2
                $keywordArgs = [];
1031 2
                foreach ($args as $arg) {
1032
                    switch ($arg[0]) {
1033
                        case "arg":
1034 17
                            if (!isset($arg[2])) {
1035 17
                                $orderedArgs[] = $this->reduce(["variable", $arg[1]]);
1036 14
                            } else {
1037 14
                                $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
1038 14
                            }
1039
                            break;
1040
1041 17
                        case "lit":
1042 17
                            $orderedArgs[] = $this->reduce($arg[1]);
1043 17
                            break;
1044
                        default:
1045
                            throw new GeneralException("Unknown arg type: " . $arg[0]);
1046 17
                    }
1047 17
                }
1048 17
1049 17
                $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
0 ignored issues
show
Documentation introduced by
$path is of type string, but the function expects a array.

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