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