Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.42% covered (warning)
88.42%
84 / 95
93.33% covered (success)
93.33%
14 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Less_ImportVisitor
88.42% covered (warning)
88.42%
84 / 95
93.33% covered (success)
93.33%
14 / 15
49.28
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 run
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 visitImport
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 processImportNode
82.26% covered (warning)
82.26%
51 / 62
0.00% covered (danger)
0.00%
0 / 1
31.07
 addVariableImport
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tryRun
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 visitDeclaration
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitAtRule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitAtRuleOut
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitMixinDefinition
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitMixinDefinitionOut
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitRuleset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitRulesetOut
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitMedia
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 visitMediaOut
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * @private
4 */
5class Less_ImportVisitor extends Less_Visitor {
6
7    /** @var Less_Environment */
8    public $env;
9    /** @var array<array{function:string,args:array}> */
10    public $variableImports = [];
11    /** @var array<string,true> */
12    public $recursionDetector = [];
13
14    /** @var int */
15    public $_currentDepth = 0;
16    /** @var mixed */
17    public $importItem;
18
19    public function __construct( $env ) {
20        parent::__construct();
21        // NOTE: Upstream creates a new environment/context here. We re-use the main one instead.
22        // This makes Less_Environment->addParsedFile() easier to support (which is custom to Less.php)
23        $this->env = $env;
24        // NOTE: Upstream `importCount` is not here, appears unused.
25        // NOTE: Upstream `isFinished` is not here, we simply call tryRun() once at the end.
26        // NOTE: Upstream `onceFileDetectionMap` is instead Less_Environment->isFileParsed.
27        // NOTE: Upstream `ImportSequencer` logic is directly inside ImportVisitor for simplicity.
28    }
29
30    public function run( $root ) {
31        $this->visitObj( $root );
32        $this->tryRun();
33    }
34
35    public function visitImport( $importNode, &$visitDeeper ) {
36        $inlineCSS = $importNode->options['inline'];
37
38        if ( !$importNode->css || $inlineCSS ) {
39
40            $env = $this->env->clone();
41            $importParent = $env->frames[0];
42            if ( $importNode->isVariableImport() ) {
43                $this->addVariableImport( [
44                    'function' => 'processImportNode',
45                    'args' => [ $importNode, $env, $importParent ]
46                ] );
47            } else {
48                $this->processImportNode( $importNode, $env, $importParent );
49            }
50        }
51        $visitDeeper = false;
52    }
53
54    public function processImportNode( $importNode, $env, &$importParent ) {
55        $evaldImportNode = $inlineCSS = $importNode->options['inline'];
56
57        try {
58            $evaldImportNode = $importNode->compileForImport( $env );
59        } catch ( Exception $e ) {
60            $importNode->css = true;
61        }
62
63        if ( $evaldImportNode && ( !$evaldImportNode->css || $inlineCSS ) ) {
64
65            if ( $importNode->options['multiple'] ) {
66                $env->importMultiple = true;
67            }
68
69            $tryAppendLessExtension = $evaldImportNode->css === null;
70
71            for ( $i = 0; $i < count( $importParent->rules ); $i++ ) {
72                if ( $importParent->rules[$i] === $importNode ) {
73                    $importParent->rules[$i] = $evaldImportNode;
74                    break;
75                }
76            }
77
78            // Rename $evaldImportNode to $importNode here so that we avoid avoid mistaken use
79            // of not-yet-compiled $importNode after this point, which upstream's code doesn't
80            // have access to after this point, either.
81            $importNode = $evaldImportNode;
82            unset( $evaldImportNode );
83
84            // NOTE: Upstream Less.js's ImportVisitor puts the rest of the processImportNode logic
85            // into a separate ImportVisitor.prototype.onImported function, because file loading
86            // is async there. They implement and call:
87            //
88            // - ImportSequencer.prototype.addImport:
89            //   remembers what processImportNode() was doing, and will call onImported
90            //   once the async file load is finished.
91            // - ImportManager.prototype.push:
92            //   resolves the import path to full path and uri,
93            //   then parses the file content into a root Ruleset for that file.
94            // - ImportVisitor.prototype.onImported:
95            //   marks the file as parsed (for skipping duplicates, to avoid recursion),
96            //   and calls tryRun() if this is the last remaining import.
97            //
98            // In PHP we load files synchronously, so we can put a simpler version of this
99            // logic directly here.
100
101            // @see less-2.5.3.js#ImportManager.prototype.push
102
103            // NOTE: This is the equivalent to upstream `newFileInfo` and `fileManager.getPath()`
104
105            $path = $importNode->getPath();
106
107            if ( $tryAppendLessExtension ) {
108                    $path = preg_match( '/(\.[a-z]*$)|([\?;].*)$/', $path ) ? $path : $path . '.less';
109            }
110
111            [ $fullPath, $uri ] =
112                Less_FileManager::getFilePath( $path, $importNode->currentFileInfo ) ?? [ $path, $path ];
113
114            // @see less-2.5.3.js#ImportManager.prototype.push/loadFileCallback
115
116            // NOTE: Upstream creates the next `currentFileInfo` here as `newFileInfo`
117            // We instead let Less_Parser::SetFileInfo() do that later via Less_Parser::parseFile().
118            // This means that instead of setting `newFileInfo.reference` we modify the $env,
119            // and Less_Parser::SetFileInfo will inherit that.
120            if ( $importNode->options['reference'] ?? false ) {
121                $env->currentFileInfo['reference'] = true;
122            }
123
124            $e = null;
125            try {
126                if ( $importNode->options['inline'] ) {
127                    if ( !file_exists( $fullPath ) ) {
128                        throw new Less_Exception_Parser(
129                            sprintf( 'File `%s` not found.', $fullPath ),
130                            null,
131                            $importNode->index,
132                            $importNode->currentFileInfo
133                        );
134                    }
135                    $root = file_get_contents( $fullPath );
136                } else {
137                    $parser = new Less_Parser( $env );
138                    // NOTE: Upstream sets `env->processImports = false` here to avoid
139                    // running ImportVisitor again (infinite loop). We instead separate
140                    // Less_Parser->parseFile() from Less_Parser->getCss(),
141                    // and only getCss() runs ImportVisitor.
142                    $root = $parser->parseFile( $fullPath, $uri, true );
143                }
144            } catch ( Less_Exception_Parser $err ) {
145                $e = $err;
146            }
147
148            // @see less-2.5.3.js#ImportManager.prototype.push/fileParsedFunc
149
150            if ( $importNode->options['optional'] && $e ) {
151                $e = null;
152                $root = new Less_Tree_Ruleset( null, [] );
153                $fullPath = null;
154            }
155
156            // @see less-2.5.3.js#ImportVisitor.prototype.onImported
157
158            if ( $e instanceof Less_Exception_Parser ) {
159                if ( !is_numeric( $e->index ) ) {
160                    $e->index = $importNode->index;
161                    $e->currentFile = $importNode->currentFileInfo;
162                    $e->genMessage();
163                }
164                throw $e;
165            }
166
167            $duplicateImport = isset( $this->recursionDetector[$fullPath] );
168
169            if ( !$env->importMultiple ) {
170                if ( $duplicateImport ) {
171                    $importNode->doSkip = true;
172                } else {
173                    // NOTE: Upstream implements skip() as dynamic function.
174                    // We instead have a regular Less_Tree_Import::skip() method,
175                    // and in cases where skip() would be re-defined here we set doSkip=null.
176                    $importNode->doSkip = null;
177                }
178            }
179
180            if ( !$fullPath && $importNode->options['optional'] ) {
181                $importNode->doSkip = true;
182            }
183
184            if ( $root ) {
185                $importNode->root = $root;
186                $importNode->importedFilename = $fullPath;
187
188                if ( !$inlineCSS && ( $env->importMultiple || !$duplicateImport ) && $fullPath ) {
189                    $this->recursionDetector[$fullPath] = true;
190                    $oldContext = $this->env;
191                    $this->env = $env;
192                    $this->visitObj( $root );
193                    $this->env = $oldContext;
194                }
195            }
196        } else {
197            $this->tryRun();
198        }
199    }
200
201    public function addVariableImport( $callback ) {
202        $this->variableImports[] = $callback;
203    }
204
205    public function tryRun() {
206        while ( true ) {
207            // NOTE: Upstream keeps a `this.imports` queue here that resumes
208            // processImportNode() logic by calling onImported() after a file
209            // is finished loading. We don't need that since we load and parse
210            // synchronously within processImportNode() instead.
211
212            if ( count( $this->variableImports ) === 0 ) {
213                break;
214            }
215            $variableImport = $this->variableImports[0];
216
217            $this->variableImports = array_slice( $this->variableImports, 1 );
218            $function = $variableImport['function'];
219
220            $this->$function( ...$variableImport["args"] );
221        }
222    }
223
224    public function visitDeclaration( $declNode, $visitDeeper ) {
225        // TODO: We might need upstream's `if (… DetachedRuleset) { this.context.frames.unshift(ruleNode); }`
226        $visitDeeper = false;
227    }
228
229    // TODO: Implement less-3.13.1.js#ImportVisitor.prototype.visitDeclarationOut
230    // if (… DetachedRuleset) { this.context.frames.shift(); }
231
232    public function visitAtRule( $atRuleNode, $visitArgs ) {
233        array_unshift( $this->env->frames, $atRuleNode );
234    }
235
236    public function visitAtRuleOut( $atRuleNode ) {
237        array_shift( $this->env->frames );
238    }
239
240    public function visitMixinDefinition( $mixinDefinitionNode, $visitArgs ) {
241        array_unshift( $this->env->frames, $mixinDefinitionNode );
242    }
243
244    public function visitMixinDefinitionOut( $mixinDefinitionNode ) {
245        array_shift( $this->env->frames );
246    }
247
248    public function visitRuleset( $rulesetNode, $visitArgs ) {
249        array_unshift( $this->env->frames, $rulesetNode );
250    }
251
252    public function visitRulesetOut( $rulesetNode ) {
253        array_shift( $this->env->frames );
254    }
255
256    public function visitMedia( $mediaNode, $visitArgs ) {
257        // TODO: Upsteam does not modify $mediaNode here. Why do we?
258        $mediaNode->allExtends = [];
259        array_unshift( $this->env->frames, $mediaNode->allExtends );
260    }
261
262    public function visitMediaOut( $mediaNode ) {
263        array_shift( $this->env->frames );
264    }
265
266}