Chris@13: createParser(); Chris@13: } Chris@13: Chris@13: $this->parser = $parser; Chris@13: $this->printer = $printer ?: new Printer(); Chris@13: $this->traverser = $traverser ?: new NodeTraverser(); Chris@13: Chris@13: foreach ($this->getDefaultPasses() as $pass) { Chris@13: $this->traverser->addVisitor($pass); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get default CodeCleaner passes. Chris@13: * Chris@13: * @return array Chris@13: */ Chris@13: private function getDefaultPasses() Chris@13: { Chris@13: $useStatementPass = new UseStatementPass(); Chris@13: $namespacePass = new NamespacePass($this); Chris@13: Chris@13: // Try to add implicit `use` statements and an implicit namespace, Chris@13: // based on the file in which the `debug` call was made. Chris@13: $this->addImplicitDebugContext([$useStatementPass, $namespacePass]); Chris@13: Chris@13: return [ Chris@13: // Validation passes Chris@13: new AbstractClassPass(), Chris@13: new AssignThisVariablePass(), Chris@13: new CalledClassPass(), Chris@13: new CallTimePassByReferencePass(), Chris@13: new FinalClassPass(), Chris@13: new FunctionContextPass(), Chris@13: new FunctionReturnInWriteContextPass(), Chris@13: new InstanceOfPass(), Chris@13: new LeavePsyshAlonePass(), Chris@13: new LegacyEmptyPass(), Chris@16: new ListPass(), Chris@13: new LoopContextPass(), Chris@13: new PassableByReferencePass(), Chris@13: new ValidConstructorPass(), Chris@13: Chris@13: // Rewriting shenanigans Chris@13: $useStatementPass, // must run before the namespace pass Chris@13: new ExitPass(), Chris@13: new ImplicitReturnPass(), Chris@13: new MagicConstantsPass(), Chris@13: $namespacePass, // must run after the implicit return pass Chris@13: new RequirePass(), Chris@13: new StrictTypesPass(), Chris@13: Chris@13: // Namespace-aware validation (which depends on aforementioned shenanigans) Chris@13: new ValidClassNamePass(), Chris@13: new ValidConstantPass(), Chris@13: new ValidFunctionNamePass(), Chris@13: ]; Chris@13: } Chris@13: Chris@13: /** Chris@13: * "Warm up" code cleaner passes when we're coming from a debug call. Chris@13: * Chris@13: * This is useful, for example, for `UseStatementPass` and `NamespacePass` Chris@13: * which keep track of state between calls, to maintain the current Chris@13: * namespace and a map of use statements. Chris@13: * Chris@13: * @param array $passes Chris@13: */ Chris@13: private function addImplicitDebugContext(array $passes) Chris@13: { Chris@13: $file = $this->getDebugFile(); Chris@13: if ($file === null) { Chris@13: return; Chris@13: } Chris@13: Chris@13: try { Chris@17: $code = @\file_get_contents($file); Chris@13: if (!$code) { Chris@13: return; Chris@13: } Chris@13: Chris@13: $stmts = $this->parse($code, true); Chris@13: if ($stmts === false) { Chris@13: return; Chris@13: } Chris@13: Chris@13: // Set up a clean traverser for just these code cleaner passes Chris@13: $traverser = new NodeTraverser(); Chris@13: foreach ($passes as $pass) { Chris@13: $traverser->addVisitor($pass); Chris@13: } Chris@13: Chris@13: $traverser->traverse($stmts); Chris@13: } catch (\Throwable $e) { Chris@13: // Don't care. Chris@13: } catch (\Exception $e) { Chris@13: // Still don't care. Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Search the stack trace for a file in which the user called Psy\debug. Chris@13: * Chris@13: * @return string|null Chris@13: */ Chris@13: private static function getDebugFile() Chris@13: { Chris@17: $trace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); Chris@13: Chris@17: foreach (\array_reverse($trace) as $stackFrame) { Chris@13: if (!self::isDebugCall($stackFrame)) { Chris@13: continue; Chris@13: } Chris@13: Chris@17: if (\preg_match('/eval\(/', $stackFrame['file'])) { Chris@17: \preg_match_all('/([^\(]+)\((\d+)/', $stackFrame['file'], $matches); Chris@13: Chris@13: return $matches[1][0]; Chris@13: } Chris@13: Chris@13: return $stackFrame['file']; Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether a given backtrace frame is a call to Psy\debug. Chris@13: * Chris@13: * @param array $stackFrame Chris@13: * Chris@13: * @return bool Chris@13: */ Chris@13: private static function isDebugCall(array $stackFrame) Chris@13: { Chris@13: $class = isset($stackFrame['class']) ? $stackFrame['class'] : null; Chris@13: $function = isset($stackFrame['function']) ? $stackFrame['function'] : null; Chris@13: Chris@13: return ($class === null && $function === 'Psy\debug') || Chris@13: ($class === 'Psy\Shell' && $function === 'debug'); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Clean the given array of code. Chris@13: * Chris@13: * @throws ParseErrorException if the code is invalid PHP, and cannot be coerced into valid PHP Chris@13: * Chris@13: * @param array $codeLines Chris@13: * @param bool $requireSemicolons Chris@13: * Chris@13: * @return string|false Cleaned PHP code, False if the input is incomplete Chris@13: */ Chris@13: public function clean(array $codeLines, $requireSemicolons = false) Chris@13: { Chris@17: $stmts = $this->parse('traverser->traverse($stmts); Chris@13: Chris@13: // Work around https://github.com/nikic/PHP-Parser/issues/399 Chris@17: $oldLocale = \setlocale(LC_NUMERIC, 0); Chris@17: \setlocale(LC_NUMERIC, 'C'); Chris@13: Chris@13: $code = $this->printer->prettyPrint($stmts); Chris@13: Chris@13: // Now put the locale back Chris@17: \setlocale(LC_NUMERIC, $oldLocale); Chris@13: Chris@13: return $code; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the current local namespace. Chris@13: * Chris@13: * @param null|array $namespace (default: null) Chris@13: * Chris@13: * @return null|array Chris@13: */ Chris@13: public function setNamespace(array $namespace = null) Chris@13: { Chris@13: $this->namespace = $namespace; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the current local namespace. Chris@13: * Chris@13: * @return null|array Chris@13: */ Chris@13: public function getNamespace() Chris@13: { Chris@13: return $this->namespace; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Lex and parse a block of code. Chris@13: * Chris@13: * @see Parser::parse Chris@13: * Chris@13: * @throws ParseErrorException for parse errors that can't be resolved by Chris@13: * waiting a line to see what comes next Chris@13: * Chris@13: * @param string $code Chris@13: * @param bool $requireSemicolons Chris@13: * Chris@13: * @return array|false A set of statements, or false if incomplete Chris@13: */ Chris@13: protected function parse($code, $requireSemicolons = false) Chris@13: { Chris@13: try { Chris@13: return $this->parser->parse($code); Chris@13: } catch (\PhpParser\Error $e) { Chris@13: if ($this->parseErrorIsUnclosedString($e, $code)) { Chris@13: return false; Chris@13: } Chris@13: Chris@13: if ($this->parseErrorIsUnterminatedComment($e, $code)) { Chris@13: return false; Chris@13: } Chris@13: Chris@13: if ($this->parseErrorIsTrailingComma($e, $code)) { Chris@13: return false; Chris@13: } Chris@13: Chris@13: if (!$this->parseErrorIsEOF($e)) { Chris@13: throw ParseErrorException::fromParseError($e); Chris@13: } Chris@13: Chris@13: if ($requireSemicolons) { Chris@13: return false; Chris@13: } Chris@13: Chris@13: try { Chris@13: // Unexpected EOF, try again with an implicit semicolon Chris@13: return $this->parser->parse($code . ';'); Chris@13: } catch (\PhpParser\Error $e) { Chris@13: return false; Chris@13: } Chris@13: } Chris@13: } Chris@13: Chris@13: private function parseErrorIsEOF(\PhpParser\Error $e) Chris@13: { Chris@13: $msg = $e->getRawMessage(); Chris@13: Chris@17: return ($msg === 'Unexpected token EOF') || (\strpos($msg, 'Syntax error, unexpected EOF') !== false); Chris@13: } Chris@13: Chris@13: /** Chris@13: * A special test for unclosed single-quoted strings. Chris@13: * Chris@13: * Unlike (all?) other unclosed statements, single quoted strings have Chris@13: * their own special beautiful snowflake syntax error just for Chris@13: * themselves. Chris@13: * Chris@13: * @param \PhpParser\Error $e Chris@13: * @param string $code Chris@13: * Chris@13: * @return bool Chris@13: */ Chris@13: private function parseErrorIsUnclosedString(\PhpParser\Error $e, $code) Chris@13: { Chris@13: if ($e->getRawMessage() !== 'Syntax error, unexpected T_ENCAPSED_AND_WHITESPACE') { Chris@13: return false; Chris@13: } Chris@13: Chris@13: try { Chris@13: $this->parser->parse($code . "';"); Chris@13: } catch (\Exception $e) { Chris@13: return false; Chris@13: } Chris@13: Chris@13: return true; Chris@13: } Chris@13: Chris@13: private function parseErrorIsUnterminatedComment(\PhpParser\Error $e, $code) Chris@13: { Chris@13: return $e->getRawMessage() === 'Unterminated comment'; Chris@13: } Chris@13: Chris@13: private function parseErrorIsTrailingComma(\PhpParser\Error $e, $code) Chris@13: { Chris@17: return ($e->getRawMessage() === 'A trailing comma is not allowed here') && (\substr(\rtrim($code), -1) === ','); Chris@13: } Chris@13: }