/laravel_tintuc/vendor/psy/psysh/src/CodeCleaner.php
PHP | 403 lines | 234 code | 48 blank | 121 comment | 17 complexity | 5d897bc175586f1f403b0630f873fa38 MD5 | raw file
- <?php
- /*
- * This file is part of Psy Shell.
- *
- * (c) 2012-2020 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Psy;
- use PhpParser\NodeTraverser;
- use PhpParser\Parser;
- use PhpParser\PrettyPrinter\Standard as Printer;
- use Psy\CodeCleaner\AbstractClassPass;
- use Psy\CodeCleaner\AssignThisVariablePass;
- use Psy\CodeCleaner\CalledClassPass;
- use Psy\CodeCleaner\CallTimePassByReferencePass;
- use Psy\CodeCleaner\EmptyArrayDimFetchPass;
- use Psy\CodeCleaner\ExitPass;
- use Psy\CodeCleaner\FinalClassPass;
- use Psy\CodeCleaner\FunctionContextPass;
- use Psy\CodeCleaner\FunctionReturnInWriteContextPass;
- use Psy\CodeCleaner\ImplicitReturnPass;
- use Psy\CodeCleaner\InstanceOfPass;
- use Psy\CodeCleaner\IssetPass;
- use Psy\CodeCleaner\LabelContextPass;
- use Psy\CodeCleaner\LeavePsyshAlonePass;
- use Psy\CodeCleaner\ListPass;
- use Psy\CodeCleaner\LoopContextPass;
- use Psy\CodeCleaner\MagicConstantsPass;
- use Psy\CodeCleaner\NamespacePass;
- use Psy\CodeCleaner\PassableByReferencePass;
- use Psy\CodeCleaner\RequirePass;
- use Psy\CodeCleaner\ReturnTypePass;
- use Psy\CodeCleaner\StrictTypesPass;
- use Psy\CodeCleaner\UseStatementPass;
- use Psy\CodeCleaner\ValidClassNamePass;
- use Psy\CodeCleaner\ValidConstantPass;
- use Psy\CodeCleaner\ValidConstructorPass;
- use Psy\CodeCleaner\ValidFunctionNamePass;
- use Psy\Exception\ParseErrorException;
- /**
- * A service to clean up user input, detect parse errors before they happen,
- * and generally work around issues with the PHP code evaluation experience.
- */
- class CodeCleaner
- {
- private $yolo = false;
- private $parser;
- private $printer;
- private $traverser;
- private $namespace;
- /**
- * CodeCleaner constructor.
- *
- * @param Parser|null $parser A PhpParser Parser instance. One will be created if not explicitly supplied
- * @param Printer|null $printer A PhpParser Printer instance. One will be created if not explicitly supplied
- * @param NodeTraverser|null $traverser A PhpParser NodeTraverser instance. One will be created if not explicitly supplied
- * @param bool $yolo run without input validation
- */
- public function __construct(Parser $parser = null, Printer $printer = null, NodeTraverser $traverser = null, $yolo = false)
- {
- $this->yolo = $yolo;
- if ($parser === null) {
- $parserFactory = new ParserFactory();
- $parser = $parserFactory->createParser();
- }
- $this->parser = $parser;
- $this->printer = $printer ?: new Printer();
- $this->traverser = $traverser ?: new NodeTraverser();
- foreach ($this->getDefaultPasses() as $pass) {
- $this->traverser->addVisitor($pass);
- }
- }
- /**
- * Check whether this CodeCleaner is in YOLO mode.
- *
- * @return bool
- */
- public function yolo()
- {
- return $this->yolo;
- }
- /**
- * Get default CodeCleaner passes.
- *
- * @return array
- */
- private function getDefaultPasses()
- {
- if ($this->yolo) {
- return $this->getYoloPasses();
- }
- $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 IssetPass(),
- new LabelContextPass(),
- new LeavePsyshAlonePass(),
- new ListPass(),
- new LoopContextPass(),
- new PassableByReferencePass(),
- new ReturnTypePass(),
- new EmptyArrayDimFetchPass(),
- 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(),
- ];
- }
- /**
- * A set of code cleaner passes that don't try to do any validation, and
- * only do minimal rewriting to make things work inside the REPL.
- *
- * This list should stay in sync with the "rewriting shenanigans" in
- * getDefaultPasses above.
- *
- * @return array
- */
- private function getYoloPasses()
- {
- $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 [
- new LeavePsyshAlonePass(),
- $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(),
- ];
- }
- /**
- * "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 === Shell::class && $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('<?php '.\implode(\PHP_EOL, $codeLines).\PHP_EOL, $requireSemicolons);
- if ($stmts === false) {
- return false;
- }
- // Catch fatal errors before they happen
- $stmts = $this->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 array|null $namespace (default: null)
- *
- * @return array|null
- */
- public function setNamespace(array $namespace = null)
- {
- $this->namespace = $namespace;
- }
- /**
- * Get the current local namespace.
- *
- * @return array|null
- */
- 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) === ',');
- }
- }