4 * This file is part of Psy Shell.
6 * (c) 2012-2017 Justin Hileman
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
14 use PhpParser\NodeTraverser;
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;
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.
54 * CodeCleaner constructor.
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
60 public function __construct(Parser $parser = null, Printer $printer = null, NodeTraverser $traverser = null)
62 if ($parser === null) {
63 $parserFactory = new ParserFactory();
64 $parser = $parserFactory->createParser();
67 $this->parser = $parser;
68 $this->printer = $printer ?: new Printer();
69 $this->traverser = $traverser ?: new NodeTraverser();
71 foreach ($this->getDefaultPasses() as $pass) {
72 $this->traverser->addVisitor($pass);
77 * Get default CodeCleaner passes.
81 private function getDefaultPasses()
85 new AbstractClassPass(),
86 new AssignThisVariablePass(),
87 new CalledClassPass(),
88 new CallTimePassByReferencePass(),
90 new FunctionContextPass(),
91 new FunctionReturnInWriteContextPass(),
93 new LeavePsyshAlonePass(),
94 new LegacyEmptyPass(),
95 new LoopContextPass(),
96 new PassableByReferencePass(),
97 new StaticConstructorPass(),
99 // Rewriting shenanigans
100 new UseStatementPass(), // must run before the namespace pass
102 new ImplicitReturnPass(),
103 new MagicConstantsPass(),
104 new NamespacePass($this), // must run after the implicit return pass
106 new StrictTypesPass(),
108 // Namespace-aware validation (which depends on aforementioned shenanigans)
109 new ValidClassNamePass(),
110 new ValidConstantPass(),
111 new ValidFunctionNamePass(),
116 * Clean the given array of code.
118 * @throws ParseErrorException if the code is invalid PHP, and cannot be coerced into valid PHP
120 * @param array $codeLines
121 * @param bool $requireSemicolons
123 * @return string|false Cleaned PHP code, False if the input is incomplete
125 public function clean(array $codeLines, $requireSemicolons = false)
127 $stmts = $this->parse('<?php ' . implode(PHP_EOL, $codeLines) . PHP_EOL, $requireSemicolons);
128 if ($stmts === false) {
132 // Catch fatal errors before they happen
133 $stmts = $this->traverser->traverse($stmts);
135 // Work around https://github.com/nikic/PHP-Parser/issues/399
136 $oldLocale = setlocale(LC_NUMERIC, 0);
137 setlocale(LC_NUMERIC, 'C');
139 $code = $this->printer->prettyPrint($stmts);
141 // Now put the locale back
142 setlocale(LC_NUMERIC, $oldLocale);
148 * Set the current local namespace.
150 * @param null|array $namespace (default: null)
154 public function setNamespace(array $namespace = null)
156 $this->namespace = $namespace;
160 * Get the current local namespace.
164 public function getNamespace()
166 return $this->namespace;
170 * Lex and parse a block of code.
174 * @throws ParseErrorException for parse errors that can't be resolved by
175 * waiting a line to see what comes next
177 * @param string $code
178 * @param bool $requireSemicolons
180 * @return array|false A set of statements, or false if incomplete
182 protected function parse($code, $requireSemicolons = false)
185 return $this->parser->parse($code);
186 } catch (\PhpParser\Error $e) {
187 if ($this->parseErrorIsUnclosedString($e, $code)) {
191 if ($this->parseErrorIsUnterminatedComment($e, $code)) {
195 if ($this->parseErrorIsTrailingComma($e, $code)) {
199 if (!$this->parseErrorIsEOF($e)) {
200 throw ParseErrorException::fromParseError($e);
203 if ($requireSemicolons) {
208 // Unexpected EOF, try again with an implicit semicolon
209 return $this->parser->parse($code . ';');
210 } catch (\PhpParser\Error $e) {
216 private function parseErrorIsEOF(\PhpParser\Error $e)
218 $msg = $e->getRawMessage();
220 return ($msg === 'Unexpected token EOF') || (strpos($msg, 'Syntax error, unexpected EOF') !== false);
224 * A special test for unclosed single-quoted strings.
226 * Unlike (all?) other unclosed statements, single quoted strings have
227 * their own special beautiful snowflake syntax error just for
230 * @param \PhpParser\Error $e
231 * @param string $code
235 private function parseErrorIsUnclosedString(\PhpParser\Error $e, $code)
237 if ($e->getRawMessage() !== 'Syntax error, unexpected T_ENCAPSED_AND_WHITESPACE') {
242 $this->parser->parse($code . "';");
243 } catch (\Exception $e) {
250 private function parseErrorIsUnterminatedComment(\PhpParser\Error $e, $code)
252 return $e->getRawMessage() === 'Unterminated comment';
255 private function parseErrorIsTrailingComma(\PhpParser\Error $e, $code)
257 return ($e->getRawMessage() === 'A trailing comma is not allowed here') && (substr(rtrim($code), -1) === ',');