5db277e1365bbca0e5e54a787cfbebef9f403f2c
[yaffs-website] / vendor / psy / psysh / src / CodeCleaner.php
1 <?php
2
3 /*
4  * This file is part of Psy Shell.
5  *
6  * (c) 2012-2018 Justin Hileman
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11
12 namespace Psy;
13
14 use PhpParser\NodeTraverser;
15 use PhpParser\Parser;
16 use PhpParser\PrettyPrinter\Standard as Printer;
17 use Psy\CodeCleaner\AbstractClassPass;
18 use Psy\CodeCleaner\AssignThisVariablePass;
19 use Psy\CodeCleaner\CalledClassPass;
20 use Psy\CodeCleaner\CallTimePassByReferencePass;
21 use Psy\CodeCleaner\ExitPass;
22 use Psy\CodeCleaner\FinalClassPass;
23 use Psy\CodeCleaner\FunctionContextPass;
24 use Psy\CodeCleaner\FunctionReturnInWriteContextPass;
25 use Psy\CodeCleaner\ImplicitReturnPass;
26 use Psy\CodeCleaner\InstanceOfPass;
27 use Psy\CodeCleaner\LeavePsyshAlonePass;
28 use Psy\CodeCleaner\LegacyEmptyPass;
29 use Psy\CodeCleaner\LoopContextPass;
30 use Psy\CodeCleaner\MagicConstantsPass;
31 use Psy\CodeCleaner\NamespacePass;
32 use Psy\CodeCleaner\PassableByReferencePass;
33 use Psy\CodeCleaner\RequirePass;
34 use Psy\CodeCleaner\StrictTypesPass;
35 use Psy\CodeCleaner\UseStatementPass;
36 use Psy\CodeCleaner\ValidClassNamePass;
37 use Psy\CodeCleaner\ValidConstantPass;
38 use Psy\CodeCleaner\ValidConstructorPass;
39 use Psy\CodeCleaner\ValidFunctionNamePass;
40 use Psy\Exception\ParseErrorException;
41
42 /**
43  * A service to clean up user input, detect parse errors before they happen,
44  * and generally work around issues with the PHP code evaluation experience.
45  */
46 class CodeCleaner
47 {
48     private $parser;
49     private $printer;
50     private $traverser;
51     private $namespace;
52
53     /**
54      * CodeCleaner constructor.
55      *
56      * @param Parser        $parser    A PhpParser Parser instance. One will be created if not explicitly supplied
57      * @param Printer       $printer   A PhpParser Printer instance. One will be created if not explicitly supplied
58      * @param NodeTraverser $traverser A PhpParser NodeTraverser instance. One will be created if not explicitly supplied
59      */
60     public function __construct(Parser $parser = null, Printer $printer = null, NodeTraverser $traverser = null)
61     {
62         if ($parser === null) {
63             $parserFactory = new ParserFactory();
64             $parser        = $parserFactory->createParser();
65         }
66
67         $this->parser    = $parser;
68         $this->printer   = $printer ?: new Printer();
69         $this->traverser = $traverser ?: new NodeTraverser();
70
71         foreach ($this->getDefaultPasses() as $pass) {
72             $this->traverser->addVisitor($pass);
73         }
74     }
75
76     /**
77      * Get default CodeCleaner passes.
78      *
79      * @return array
80      */
81     private function getDefaultPasses()
82     {
83         $useStatementPass = new UseStatementPass();
84         $namespacePass    = new NamespacePass($this);
85
86         // Try to add implicit `use` statements and an implicit namespace,
87         // based on the file in which the `debug` call was made.
88         $this->addImplicitDebugContext([$useStatementPass, $namespacePass]);
89
90         return [
91             // Validation passes
92             new AbstractClassPass(),
93             new AssignThisVariablePass(),
94             new CalledClassPass(),
95             new CallTimePassByReferencePass(),
96             new FinalClassPass(),
97             new FunctionContextPass(),
98             new FunctionReturnInWriteContextPass(),
99             new InstanceOfPass(),
100             new LeavePsyshAlonePass(),
101             new LegacyEmptyPass(),
102             new LoopContextPass(),
103             new PassableByReferencePass(),
104             new ValidConstructorPass(),
105
106             // Rewriting shenanigans
107             $useStatementPass,        // must run before the namespace pass
108             new ExitPass(),
109             new ImplicitReturnPass(),
110             new MagicConstantsPass(),
111             $namespacePass,           // must run after the implicit return pass
112             new RequirePass(),
113             new StrictTypesPass(),
114
115             // Namespace-aware validation (which depends on aforementioned shenanigans)
116             new ValidClassNamePass(),
117             new ValidConstantPass(),
118             new ValidFunctionNamePass(),
119         ];
120     }
121
122     /**
123      * "Warm up" code cleaner passes when we're coming from a debug call.
124      *
125      * This is useful, for example, for `UseStatementPass` and `NamespacePass`
126      * which keep track of state between calls, to maintain the current
127      * namespace and a map of use statements.
128      *
129      * @param array $passes
130      */
131     private function addImplicitDebugContext(array $passes)
132     {
133         $file = $this->getDebugFile();
134         if ($file === null) {
135             return;
136         }
137
138         try {
139             $code = @file_get_contents($file);
140             if (!$code) {
141                 return;
142             }
143
144             $stmts = $this->parse($code, true);
145             if ($stmts === false) {
146                 return;
147             }
148
149             // Set up a clean traverser for just these code cleaner passes
150             $traverser = new NodeTraverser();
151             foreach ($passes as $pass) {
152                 $traverser->addVisitor($pass);
153             }
154
155             $traverser->traverse($stmts);
156         } catch (\Throwable $e) {
157             // Don't care.
158         } catch (\Exception $e) {
159             // Still don't care.
160         }
161     }
162
163     /**
164      * Search the stack trace for a file in which the user called Psy\debug.
165      *
166      * @return string|null
167      */
168     private static function getDebugFile()
169     {
170         $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
171
172         foreach (array_reverse($trace) as $stackFrame) {
173             if (!self::isDebugCall($stackFrame)) {
174                 continue;
175             }
176
177             if (preg_match('/eval\(/', $stackFrame['file'])) {
178                 preg_match_all('/([^\(]+)\((\d+)/', $stackFrame['file'], $matches);
179
180                 return $matches[1][0];
181             }
182
183             return $stackFrame['file'];
184         }
185     }
186
187     /**
188      * Check whether a given backtrace frame is a call to Psy\debug.
189      *
190      * @param array $stackFrame
191      *
192      * @return bool
193      */
194     private static function isDebugCall(array $stackFrame)
195     {
196         $class    = isset($stackFrame['class']) ? $stackFrame['class'] : null;
197         $function = isset($stackFrame['function']) ? $stackFrame['function'] : null;
198
199         return ($class === null && $function === 'Psy\debug') ||
200             ($class === 'Psy\Shell' && $function === 'debug');
201     }
202
203     /**
204      * Clean the given array of code.
205      *
206      * @throws ParseErrorException if the code is invalid PHP, and cannot be coerced into valid PHP
207      *
208      * @param array $codeLines
209      * @param bool  $requireSemicolons
210      *
211      * @return string|false Cleaned PHP code, False if the input is incomplete
212      */
213     public function clean(array $codeLines, $requireSemicolons = false)
214     {
215         $stmts = $this->parse('<?php ' . implode(PHP_EOL, $codeLines) . PHP_EOL, $requireSemicolons);
216         if ($stmts === false) {
217             return false;
218         }
219
220         // Catch fatal errors before they happen
221         $stmts = $this->traverser->traverse($stmts);
222
223         // Work around https://github.com/nikic/PHP-Parser/issues/399
224         $oldLocale = setlocale(LC_NUMERIC, 0);
225         setlocale(LC_NUMERIC, 'C');
226
227         $code = $this->printer->prettyPrint($stmts);
228
229         // Now put the locale back
230         setlocale(LC_NUMERIC, $oldLocale);
231
232         return $code;
233     }
234
235     /**
236      * Set the current local namespace.
237      *
238      * @param null|array $namespace (default: null)
239      *
240      * @return null|array
241      */
242     public function setNamespace(array $namespace = null)
243     {
244         $this->namespace = $namespace;
245     }
246
247     /**
248      * Get the current local namespace.
249      *
250      * @return null|array
251      */
252     public function getNamespace()
253     {
254         return $this->namespace;
255     }
256
257     /**
258      * Lex and parse a block of code.
259      *
260      * @see Parser::parse
261      *
262      * @throws ParseErrorException for parse errors that can't be resolved by
263      *                             waiting a line to see what comes next
264      *
265      * @param string $code
266      * @param bool   $requireSemicolons
267      *
268      * @return array|false A set of statements, or false if incomplete
269      */
270     protected function parse($code, $requireSemicolons = false)
271     {
272         try {
273             return $this->parser->parse($code);
274         } catch (\PhpParser\Error $e) {
275             if ($this->parseErrorIsUnclosedString($e, $code)) {
276                 return false;
277             }
278
279             if ($this->parseErrorIsUnterminatedComment($e, $code)) {
280                 return false;
281             }
282
283             if ($this->parseErrorIsTrailingComma($e, $code)) {
284                 return false;
285             }
286
287             if (!$this->parseErrorIsEOF($e)) {
288                 throw ParseErrorException::fromParseError($e);
289             }
290
291             if ($requireSemicolons) {
292                 return false;
293             }
294
295             try {
296                 // Unexpected EOF, try again with an implicit semicolon
297                 return $this->parser->parse($code . ';');
298             } catch (\PhpParser\Error $e) {
299                 return false;
300             }
301         }
302     }
303
304     private function parseErrorIsEOF(\PhpParser\Error $e)
305     {
306         $msg = $e->getRawMessage();
307
308         return ($msg === 'Unexpected token EOF') || (strpos($msg, 'Syntax error, unexpected EOF') !== false);
309     }
310
311     /**
312      * A special test for unclosed single-quoted strings.
313      *
314      * Unlike (all?) other unclosed statements, single quoted strings have
315      * their own special beautiful snowflake syntax error just for
316      * themselves.
317      *
318      * @param \PhpParser\Error $e
319      * @param string           $code
320      *
321      * @return bool
322      */
323     private function parseErrorIsUnclosedString(\PhpParser\Error $e, $code)
324     {
325         if ($e->getRawMessage() !== 'Syntax error, unexpected T_ENCAPSED_AND_WHITESPACE') {
326             return false;
327         }
328
329         try {
330             $this->parser->parse($code . "';");
331         } catch (\Exception $e) {
332             return false;
333         }
334
335         return true;
336     }
337
338     private function parseErrorIsUnterminatedComment(\PhpParser\Error $e, $code)
339     {
340         return $e->getRawMessage() === 'Unterminated comment';
341     }
342
343     private function parseErrorIsTrailingComma(\PhpParser\Error $e, $code)
344     {
345         return ($e->getRawMessage() === 'A trailing comma is not allowed here') && (substr(rtrim($code), -1) === ',');
346     }
347 }