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