d2c152303fffa8f67641ffa19d55440ef24a326a
[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\FunctionReturnInWriteContextPass;
24 use Psy\CodeCleaner\ImplicitReturnPass;
25 use Psy\CodeCleaner\InstanceOfPass;
26 use Psy\CodeCleaner\LeavePsyshAlonePass;
27 use Psy\CodeCleaner\LegacyEmptyPass;
28 use Psy\CodeCleaner\MagicConstantsPass;
29 use Psy\CodeCleaner\NamespacePass;
30 use Psy\CodeCleaner\PassableByReferencePass;
31 use Psy\CodeCleaner\StaticConstructorPass;
32 use Psy\CodeCleaner\StrictTypesPass;
33 use Psy\CodeCleaner\UseStatementPass;
34 use Psy\CodeCleaner\ValidClassNamePass;
35 use Psy\CodeCleaner\ValidConstantPass;
36 use Psy\CodeCleaner\ValidFunctionNamePass;
37 use Psy\Exception\ParseErrorException;
38
39 /**
40  * A service to clean up user input, detect parse errors before they happen,
41  * and generally work around issues with the PHP code evaluation experience.
42  */
43 class CodeCleaner
44 {
45     private $parser;
46     private $printer;
47     private $traverser;
48     private $namespace;
49
50     /**
51      * CodeCleaner constructor.
52      *
53      * @param Parser        $parser    A PhpParser Parser instance. One will be created if not explicitly supplied
54      * @param Printer       $printer   A PhpParser Printer instance. One will be created if not explicitly supplied
55      * @param NodeTraverser $traverser A PhpParser NodeTraverser instance. One will be created if not explicitly supplied
56      */
57     public function __construct(Parser $parser = null, Printer $printer = null, NodeTraverser $traverser = null)
58     {
59         if ($parser === null) {
60             $parserFactory = new ParserFactory();
61             $parser        = $parserFactory->createParser();
62         }
63
64         $this->parser    = $parser;
65         $this->printer   = $printer ?: new Printer();
66         $this->traverser = $traverser ?: new NodeTraverser();
67
68         foreach ($this->getDefaultPasses() as $pass) {
69             $this->traverser->addVisitor($pass);
70         }
71     }
72
73     /**
74      * Get default CodeCleaner passes.
75      *
76      * @return array
77      */
78     private function getDefaultPasses()
79     {
80         return array(
81             new AbstractClassPass(),
82             new AssignThisVariablePass(),
83             new FunctionReturnInWriteContextPass(),
84             new CallTimePassByReferencePass(),
85             new PassableByReferencePass(),
86             new CalledClassPass(),
87             new FinalClassPass(),
88             new InstanceOfPass(),
89             new LeavePsyshAlonePass(),
90             new LegacyEmptyPass(),
91             new ImplicitReturnPass(),
92             new UseStatementPass(),      // must run before namespace and validation passes
93             new NamespacePass($this),    // must run after the implicit return pass
94             new StrictTypesPass(),
95             new StaticConstructorPass(),
96             new ValidFunctionNamePass(),
97             new ValidClassNamePass(),
98             new ValidConstantPass(),
99             new MagicConstantsPass(),
100             new ExitPass(),
101         );
102     }
103
104     /**
105      * Clean the given array of code.
106      *
107      * @throws ParseErrorException if the code is invalid PHP, and cannot be coerced into valid PHP
108      *
109      * @param array $codeLines
110      * @param bool  $requireSemicolons
111      *
112      * @return string|false Cleaned PHP code, False if the input is incomplete
113      */
114     public function clean(array $codeLines, $requireSemicolons = false)
115     {
116         $stmts = $this->parse('<?php ' . implode(PHP_EOL, $codeLines) . PHP_EOL, $requireSemicolons);
117         if ($stmts === false) {
118             return false;
119         }
120
121         // Catch fatal errors before they happen
122         $stmts = $this->traverser->traverse($stmts);
123
124         return $this->printer->prettyPrint($stmts);
125     }
126
127     /**
128      * Set the current local namespace.
129      *
130      * @param null|array $namespace (default: null)
131      *
132      * @return null|array
133      */
134     public function setNamespace(array $namespace = null)
135     {
136         $this->namespace = $namespace;
137     }
138
139     /**
140      * Get the current local namespace.
141      *
142      * @return null|array
143      */
144     public function getNamespace()
145     {
146         return $this->namespace;
147     }
148
149     /**
150      * Lex and parse a block of code.
151      *
152      * @see Parser::parse
153      *
154      * @throws ParseErrorException for parse errors that can't be resolved by
155      *                             waiting a line to see what comes next
156      *
157      * @param string $code
158      * @param bool   $requireSemicolons
159      *
160      * @return array|false A set of statements, or false if incomplete
161      */
162     protected function parse($code, $requireSemicolons = false)
163     {
164         try {
165             return $this->parser->parse($code);
166         } catch (\PhpParser\Error $e) {
167             if ($this->parseErrorIsUnclosedString($e, $code)) {
168                 return false;
169             }
170
171             if ($this->parseErrorIsUnterminatedComment($e, $code)) {
172                 return false;
173             }
174
175             if (!$this->parseErrorIsEOF($e)) {
176                 throw ParseErrorException::fromParseError($e);
177             }
178
179             if ($requireSemicolons) {
180                 return false;
181             }
182
183             try {
184                 // Unexpected EOF, try again with an implicit semicolon
185                 return $this->parser->parse($code . ';');
186             } catch (\PhpParser\Error $e) {
187                 return false;
188             }
189         }
190     }
191
192     private function parseErrorIsEOF(\PhpParser\Error $e)
193     {
194         $msg = $e->getRawMessage();
195
196         return ($msg === 'Unexpected token EOF') || (strpos($msg, 'Syntax error, unexpected EOF') !== false);
197     }
198
199     /**
200      * A special test for unclosed single-quoted strings.
201      *
202      * Unlike (all?) other unclosed statements, single quoted strings have
203      * their own special beautiful snowflake syntax error just for
204      * themselves.
205      *
206      * @param \PhpParser\Error $e
207      * @param string           $code
208      *
209      * @return bool
210      */
211     private function parseErrorIsUnclosedString(\PhpParser\Error $e, $code)
212     {
213         if ($e->getRawMessage() !== 'Syntax error, unexpected T_ENCAPSED_AND_WHITESPACE') {
214             return false;
215         }
216
217         try {
218             $this->parser->parse($code . "';");
219         } catch (\Exception $e) {
220             return false;
221         }
222
223         return true;
224     }
225
226     private function parseErrorIsUnterminatedComment(\PhpParser\Error $e, $code)
227     {
228         return $e->getRawMessage() === 'Unterminated comment';
229     }
230 }