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