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