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
 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            for ( $i = 0; $i < count( $importParent->rules ); $i++ ) {
63                if ( $importParent->rules[$i] === $importNode ) {
64                    $importParent->rules[$i] = $evaldImportNode;
65                    break;
66                }
67            }
68
69            // Rename $evaldImportNode to $importNode here so that we avoid avoid mistaken use
70            // of not-yet-compiled $importNode after this point, which upstream's code doesn't
71            // have access to after this point, either.
72            $importNode = $evaldImportNode;
73            unset( $evaldImportNode );
74
75            // NOTE: Upstream Less.js's ImportVisitor puts the rest of the processImportNode logic
76            // into a separate ImportVisitor.prototype.onImported function, because file loading
77            // is async there. They implement and call:
78            //
79            // - ImportSequencer.prototype.addImport:
80            //   remembers what processImportNode() was doing, and will call onImported
81            //   once the async file load is finished.
82            // - ImportManager.prototype.push:
83            //   resolves the import path to full path and uri,
84            //   then parses the file content into a root Ruleset for that file.
85            // - ImportVisitor.prototype.onImported:
86            //   marks the file as parsed (for skipping duplicates, to avoid recursion),
87            //   and calls tryRun() if this is the last remaining import.
88            //
89            // In PHP we load files synchronously, so we can put a simpler version of this
90            // logic directly here.
91
92            // @see less-2.5.3.js#ImportManager.prototype.push
93
94            // NOTE: This is the equivalent to upstream `newFileInfo` and `fileManager.getPath()`
95            // TODO: Move this into Less_Tree_Import->PathAndUri along with the rest of the
96            // import dirs resolution logic.
97            // TODO: We might need upstream's `tryAppendLessExtension` logic here.
98            //       We currenlty do do this in getPath instead.
99
100            $path_and_uri = $env->callImportCallback( $importNode );
101            if ( !$path_and_uri ) {
102                $path_and_uri = $importNode->PathAndUri();
103            }
104            if ( $path_and_uri ) {
105                [ $full_path, $uri ] = $path_and_uri;
106            } else {
107                $full_path = $uri = $importNode->getPath();
108            }
109            '@phan-var string $full_path';
110
111            // @see less-2.5.3.js#ImportManager.prototype.push/loadFileCallback
112
113            // NOTE: Upstream creates the next `currentFileInfo` here as `newFileInfo`
114            // We instead let Less_Parser::SetFileInfo() do that later via Less_Parser::parseFile().
115            // This means that instead of setting `newFileInfo.reference` we modify the $env,
116            // and Less_Parser::SetFileInfo will inherit that.
117            if ( $importNode->options['reference'] ?? false ) {
118                $env->currentFileInfo['reference'] = true;
119            }
120
121            $e = null;
122            try {
123                if ( $importNode->options['inline'] ) {
124                    if ( !file_exists( $full_path ) ) {
125                        throw new Less_Exception_Parser(
126                            sprintf( 'File `%s` not found.', $full_path ),
127                            null,
128                            $importNode->index,
129                            $importNode->currentFileInfo
130                        );
131                    }
132                    $root = file_get_contents( $full_path );
133                } else {
134                    $parser = new Less_Parser( $env );
135                    // NOTE: Upstream sets `env->processImports = false` here to avoid
136                    // running ImportVisitor again (infinite loop). We instead separate
137                    // Less_Parser->parseFile() from Less_Parser->getCss(),
138                    // and only getCss() runs ImportVisitor.
139                    $root = $parser->parseFile( $full_path, $uri, true );
140                }
141            } catch ( Less_Exception_Parser $err ) {
142                $e = $err;
143            }
144
145            // @see less-2.5.3.js#ImportManager.prototype.push/fileParsedFunc
146
147            if ( $importNode->options['optional'] && $e ) {
148                $e = null;
149                $root = new Less_Tree_Ruleset( null, [] );
150                $full_path = null;
151            }
152
153            // @see less-2.5.3.js#ImportVisitor.prototype.onImported
154
155            if ( $e instanceof Less_Exception_Parser ) {
156                if ( !is_numeric( $e->index ) ) {
157                    $e->index = $importNode->index;
158                    $e->currentFile = $importNode->currentFileInfo;
159                    $e->genMessage();
160                }
161                throw $e;
162            }
163
164            $duplicateImport = isset( $this->recursionDetector[$full_path] );
165
166            if ( !$env->importMultiple ) {
167                if ( $duplicateImport ) {
168                    $importNode->doSkip = true;
169                } else {
170                    // NOTE: Upstream implements skip() as dynamic function.
171                    // We instead have a regular Less_Tree_Import::skip() method,
172                    // and in cases where skip() would be re-defined here we set doSkip=null.
173                    $importNode->doSkip = null;
174                }
175            }
176
177            if ( !$full_path && $importNode->options['optional'] ) {
178                $importNode->doSkip = true;
179            }
180
181            if ( $root ) {
182                $importNode->root = $root;
183                $importNode->importedFilename = $full_path;
184
185                if ( !$inlineCSS && ( $env->importMultiple || !$duplicateImport ) && $full_path ) {
186                    $this->recursionDetector[$full_path] = true;
187                    $oldContext = $this->env;
188                    $this->env = $env;
189                    $this->visitObj( $root );
190                    $this->env = $oldContext;
191                }
192            }
193        } else {
194            $this->tryRun();
195        }
196    }
197
198    public function addVariableImport( $callback ) {
199        $this->variableImports[] = $callback;
200    }
201
202    public function tryRun() {
203        while ( true ) {
204            // NOTE: Upstream keeps a `this.imports` queue here that resumes
205            // processImportNode() logic by calling onImported() after a file
206            // is finished loading. We don't need that since we load and parse
207            // synchronously within processImportNode() instead.
208
209            if ( count( $this->variableImports ) === 0 ) {
210                break;
211            }
212            $variableImport = $this->variableImports[0];
213
214            $this->variableImports = array_slice( $this->variableImports, 1 );
215            $function = $variableImport['function'];
216
217            $this->$function( ...$variableImport["args"] );
218        }
219    }
220
221    public function visitRule( $ruleNode, $visitDeeper ) {
222        // TODO: We might need upstream's `if (… DetachedRuleset) { this.context.frames.unshift(ruleNode); }`
223        $visitDeeper = false;
224    }
225
226    // TODO: We might need upstream's visitRuleOut()
227    // if (… DetachedRuleset) { this.context.frames.shift(); }
228
229    public function visitDirective( $directiveNode, $visitArgs ) {
230        array_unshift( $this->env->frames, $directiveNode );
231    }
232
233    public function visitDirectiveOut( $directiveNode ) {
234        array_shift( $this->env->frames );
235    }
236
237    public function visitMixinDefinition( $mixinDefinitionNode, $visitArgs ) {
238        array_unshift( $this->env->frames, $mixinDefinitionNode );
239    }
240
241    public function visitMixinDefinitionOut( $mixinDefinitionNode ) {
242        array_shift( $this->env->frames );
243    }
244
245    public function visitRuleset( $rulesetNode, $visitArgs ) {
246        array_unshift( $this->env->frames, $rulesetNode );
247    }
248
249    public function visitRulesetOut( $rulesetNode ) {
250        array_shift( $this->env->frames );
251    }
252
253    public function visitMedia( $mediaNode, $visitArgs ) {
254        // TODO: Upsteam does not modify $mediaNode here. Why do we?
255        $mediaNode->allExtends = [];
256        array_unshift( $this->env->frames, $mediaNode->allExtends );
257    }
258
259    public function visitMediaOut( $mediaNode ) {
260        array_shift( $this->env->frames );
261    }
262
263}