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