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