fa3a878576249b8d6e228015ddbc83da0e55638b
[yaffs-website] / vendor / psy / psysh / src / Psy / CodeCleaner.php
1 <?php
2
3 /*
4  * This file is part of Psy Shell.
5  *
6  * (c) 2012-2017 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\StaticConstructorPass;
35 use Psy\CodeCleaner\StrictTypesPass;
36 use Psy\CodeCleaner\UseStatementPass;
37 use Psy\CodeCleaner\ValidClassNamePass;
38 use Psy\CodeCleaner\ValidConstantPass;
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         return array(
84             // Validation passes
85             new AbstractClassPass(),
86             new AssignThisVariablePass(),
87             new CalledClassPass(),
88             new CallTimePassByReferencePass(),
89             new FinalClassPass(),
90             new FunctionContextPass(),
91             new FunctionReturnInWriteContextPass(),
92             new InstanceOfPass(),
93             new LeavePsyshAlonePass(),
94             new LegacyEmptyPass(),
95             new LoopContextPass(),
96             new PassableByReferencePass(),
97             new StaticConstructorPass(),
98
99             // Rewriting shenanigans
100             new UseStatementPass(),   // must run before the namespace pass
101             new ExitPass(),
102             new ImplicitReturnPass(),
103             new MagicConstantsPass(),
104             new NamespacePass($this), // must run after the implicit return pass
105             new RequirePass(),
106             new StrictTypesPass(),
107
108             // Namespace-aware validation (which depends on aforementioned shenanigans)
109             new ValidClassNamePass(),
110             new ValidConstantPass(),
111             new ValidFunctionNamePass(),
112         );
113     }
114
115     /**
116      * Clean the given array of code.
117      *
118      * @throws ParseErrorException if the code is invalid PHP, and cannot be coerced into valid PHP
119      *
120      * @param array $codeLines
121      * @param bool  $requireSemicolons
122      *
123      * @return string|false Cleaned PHP code, False if the input is incomplete
124      */
125     public function clean(array $codeLines, $requireSemicolons = false)
126     {
127         $stmts = $this->parse('<?php ' . implode(PHP_EOL, $codeLines) . PHP_EOL, $requireSemicolons);
128         if ($stmts === false) {
129             return false;
130         }
131
132         // Catch fatal errors before they happen
133         $stmts = $this->traverser->traverse($stmts);
134
135         // Work around https://github.com/nikic/PHP-Parser/issues/399
136         $oldLocale = setlocale(LC_NUMERIC, 0);
137         setlocale(LC_NUMERIC, 'C');
138
139         $code = $this->printer->prettyPrint($stmts);
140
141         // Now put the locale back
142         setlocale(LC_NUMERIC, $oldLocale);
143
144         return $code;
145     }
146
147     /**
148      * Set the current local namespace.
149      *
150      * @param null|array $namespace (default: null)
151      *
152      * @return null|array
153      */
154     public function setNamespace(array $namespace = null)
155     {
156         $this->namespace = $namespace;
157     }
158
159     /**
160      * Get the current local namespace.
161      *
162      * @return null|array
163      */
164     public function getNamespace()
165     {
166         return $this->namespace;
167     }
168
169     /**
170      * Lex and parse a block of code.
171      *
172      * @see Parser::parse
173      *
174      * @throws ParseErrorException for parse errors that can't be resolved by
175      *                             waiting a line to see what comes next
176      *
177      * @param string $code
178      * @param bool   $requireSemicolons
179      *
180      * @return array|false A set of statements, or false if incomplete
181      */
182     protected function parse($code, $requireSemicolons = false)
183     {
184         try {
185             return $this->parser->parse($code);
186         } catch (\PhpParser\Error $e) {
187             if ($this->parseErrorIsUnclosedString($e, $code)) {
188                 return false;
189             }
190
191             if ($this->parseErrorIsUnterminatedComment($e, $code)) {
192                 return false;
193             }
194
195             if ($this->parseErrorIsTrailingComma($e, $code)) {
196                 return false;
197             }
198
199             if (!$this->parseErrorIsEOF($e)) {
200                 throw ParseErrorException::fromParseError($e);
201             }
202
203             if ($requireSemicolons) {
204                 return false;
205             }
206
207             try {
208                 // Unexpected EOF, try again with an implicit semicolon
209                 return $this->parser->parse($code . ';');
210             } catch (\PhpParser\Error $e) {
211                 return false;
212             }
213         }
214     }
215
216     private function parseErrorIsEOF(\PhpParser\Error $e)
217     {
218         $msg = $e->getRawMessage();
219
220         return ($msg === 'Unexpected token EOF') || (strpos($msg, 'Syntax error, unexpected EOF') !== false);
221     }
222
223     /**
224      * A special test for unclosed single-quoted strings.
225      *
226      * Unlike (all?) other unclosed statements, single quoted strings have
227      * their own special beautiful snowflake syntax error just for
228      * themselves.
229      *
230      * @param \PhpParser\Error $e
231      * @param string           $code
232      *
233      * @return bool
234      */
235     private function parseErrorIsUnclosedString(\PhpParser\Error $e, $code)
236     {
237         if ($e->getRawMessage() !== 'Syntax error, unexpected T_ENCAPSED_AND_WHITESPACE') {
238             return false;
239         }
240
241         try {
242             $this->parser->parse($code . "';");
243         } catch (\Exception $e) {
244             return false;
245         }
246
247         return true;
248     }
249
250     private function parseErrorIsUnterminatedComment(\PhpParser\Error $e, $code)
251     {
252         return $e->getRawMessage() === 'Unterminated comment';
253     }
254
255     private function parseErrorIsTrailingComma(\PhpParser\Error $e, $code)
256     {
257         return ($e->getRawMessage() === 'A trailing comma is not allowed here') && (substr(rtrim($code), -1) === ',');
258     }
259 }