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\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;
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.
53 * CodeCleaner constructor.
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
59 public function __construct(Parser $parser = null, Printer $printer = null, NodeTraverser $traverser = null)
61 if ($parser === null) {
62 $parserFactory = new ParserFactory();
63 $parser = $parserFactory->createParser();
66 $this->parser = $parser;
67 $this->printer = $printer ?: new Printer();
68 $this->traverser = $traverser ?: new NodeTraverser();
70 foreach ($this->getDefaultPasses() as $pass) {
71 $this->traverser->addVisitor($pass);
76 * Get default CodeCleaner passes.
80 private function getDefaultPasses()
83 new AbstractClassPass(),
84 new AssignThisVariablePass(),
85 new FunctionReturnInWriteContextPass(),
86 new CallTimePassByReferencePass(),
87 new PassableByReferencePass(),
88 new CalledClassPass(),
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
98 new StrictTypesPass(),
99 new StaticConstructorPass(),
100 new ValidFunctionNamePass(),
101 new ValidClassNamePass(),
102 new ValidConstantPass(),
103 new MagicConstantsPass(),
109 * Clean the given array of code.
111 * @throws ParseErrorException if the code is invalid PHP, and cannot be coerced into valid PHP
113 * @param array $codeLines
114 * @param bool $requireSemicolons
116 * @return string|false Cleaned PHP code, False if the input is incomplete
118 public function clean(array $codeLines, $requireSemicolons = false)
120 $stmts = $this->parse('<?php ' . implode(PHP_EOL, $codeLines) . PHP_EOL, $requireSemicolons);
121 if ($stmts === false) {
125 // Catch fatal errors before they happen
126 $stmts = $this->traverser->traverse($stmts);
128 return $this->printer->prettyPrint($stmts);
132 * Set the current local namespace.
134 * @param null|array $namespace (default: null)
138 public function setNamespace(array $namespace = null)
140 $this->namespace = $namespace;
144 * Get the current local namespace.
148 public function getNamespace()
150 return $this->namespace;
154 * Lex and parse a block of code.
158 * @throws ParseErrorException for parse errors that can't be resolved by
159 * waiting a line to see what comes next
161 * @param string $code
162 * @param bool $requireSemicolons
164 * @return array|false A set of statements, or false if incomplete
166 protected function parse($code, $requireSemicolons = false)
169 return $this->parser->parse($code);
170 } catch (\PhpParser\Error $e) {
171 if ($this->parseErrorIsUnclosedString($e, $code)) {
175 if ($this->parseErrorIsUnterminatedComment($e, $code)) {
179 if (!$this->parseErrorIsEOF($e)) {
180 throw ParseErrorException::fromParseError($e);
183 if ($requireSemicolons) {
188 // Unexpected EOF, try again with an implicit semicolon
189 return $this->parser->parse($code . ';');
190 } catch (\PhpParser\Error $e) {
196 private function parseErrorIsEOF(\PhpParser\Error $e)
198 $msg = $e->getRawMessage();
200 return ($msg === 'Unexpected token EOF') || (strpos($msg, 'Syntax error, unexpected EOF') !== false);
204 * A special test for unclosed single-quoted strings.
206 * Unlike (all?) other unclosed statements, single quoted strings have
207 * their own special beautiful snowflake syntax error just for
210 * @param \PhpParser\Error $e
211 * @param string $code
215 private function parseErrorIsUnclosedString(\PhpParser\Error $e, $code)
217 if ($e->getRawMessage() !== 'Syntax error, unexpected T_ENCAPSED_AND_WHITESPACE') {
222 $this->parser->parse($code . "';");
223 } catch (\Exception $e) {
230 private function parseErrorIsUnterminatedComment(\PhpParser\Error $e, $code)
232 return $e->getRawMessage() === 'Unterminated comment';