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\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;
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.
51 * CodeCleaner constructor.
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
57 public function __construct(Parser $parser = null, Printer $printer = null, NodeTraverser $traverser = null)
59 if ($parser === null) {
60 $parserFactory = new ParserFactory();
61 $parser = $parserFactory->createParser();
64 $this->parser = $parser;
65 $this->printer = $printer ?: new Printer();
66 $this->traverser = $traverser ?: new NodeTraverser();
68 foreach ($this->getDefaultPasses() as $pass) {
69 $this->traverser->addVisitor($pass);
74 * Get default CodeCleaner passes.
78 private function getDefaultPasses()
81 new AbstractClassPass(),
82 new AssignThisVariablePass(),
83 new FunctionReturnInWriteContextPass(),
84 new CallTimePassByReferencePass(),
85 new PassableByReferencePass(),
86 new CalledClassPass(),
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(),
105 * Clean the given array of code.
107 * @throws ParseErrorException if the code is invalid PHP, and cannot be coerced into valid PHP
109 * @param array $codeLines
110 * @param bool $requireSemicolons
112 * @return string|false Cleaned PHP code, False if the input is incomplete
114 public function clean(array $codeLines, $requireSemicolons = false)
116 $stmts = $this->parse('<?php ' . implode(PHP_EOL, $codeLines) . PHP_EOL, $requireSemicolons);
117 if ($stmts === false) {
121 // Catch fatal errors before they happen
122 $stmts = $this->traverser->traverse($stmts);
124 return $this->printer->prettyPrint($stmts);
128 * Set the current local namespace.
130 * @param null|array $namespace (default: null)
134 public function setNamespace(array $namespace = null)
136 $this->namespace = $namespace;
140 * Get the current local namespace.
144 public function getNamespace()
146 return $this->namespace;
150 * Lex and parse a block of code.
154 * @throws ParseErrorException for parse errors that can't be resolved by
155 * waiting a line to see what comes next
157 * @param string $code
158 * @param bool $requireSemicolons
160 * @return array|false A set of statements, or false if incomplete
162 protected function parse($code, $requireSemicolons = false)
165 return $this->parser->parse($code);
166 } catch (\PhpParser\Error $e) {
167 if ($this->parseErrorIsUnclosedString($e, $code)) {
171 if ($this->parseErrorIsUnterminatedComment($e, $code)) {
175 if (!$this->parseErrorIsEOF($e)) {
176 throw ParseErrorException::fromParseError($e);
179 if ($requireSemicolons) {
184 // Unexpected EOF, try again with an implicit semicolon
185 return $this->parser->parse($code . ';');
186 } catch (\PhpParser\Error $e) {
192 private function parseErrorIsEOF(\PhpParser\Error $e)
194 $msg = $e->getRawMessage();
196 return ($msg === 'Unexpected token EOF') || (strpos($msg, 'Syntax error, unexpected EOF') !== false);
200 * A special test for unclosed single-quoted strings.
202 * Unlike (all?) other unclosed statements, single quoted strings have
203 * their own special beautiful snowflake syntax error just for
206 * @param \PhpParser\Error $e
207 * @param string $code
211 private function parseErrorIsUnclosedString(\PhpParser\Error $e, $code)
213 if ($e->getRawMessage() !== 'Syntax error, unexpected T_ENCAPSED_AND_WHITESPACE') {
218 $this->parser->parse($code . "';");
219 } catch (\Exception $e) {
226 private function parseErrorIsUnterminatedComment(\PhpParser\Error $e, $code)
228 return $e->getRawMessage() === 'Unterminated comment';