First commit

This commit is contained in:
Theodotos Andreou 2018-01-14 13:10:16 +00:00
commit c6e2478c40
13918 changed files with 2303184 additions and 0 deletions

45
vendor/psy/psysh/src/Psy/Autoloader.php vendored Normal file
View file

@ -0,0 +1,45 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy;
/**
* Psy class autoloader.
*/
class Autoloader
{
/**
* Register autoload() as an SPL autoloader.
*
* @see self::autoload
*/
public static function register()
{
spl_autoload_register(array(__CLASS__, 'autoload'));
}
/**
* Autoload Psy classes.
*
* @param string $class
*/
public static function autoload($class)
{
if (0 !== strpos($class, 'Psy')) {
return;
}
$file = dirname(__DIR__) . '/' . strtr($class, '\\', '/') . '.php';
if (is_file($file)) {
require $file;
}
}
}

259
vendor/psy/psysh/src/Psy/CodeCleaner.php vendored Normal file
View file

@ -0,0 +1,259 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 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\ExitPass;
use Psy\CodeCleaner\FinalClassPass;
use Psy\CodeCleaner\FunctionContextPass;
use Psy\CodeCleaner\FunctionReturnInWriteContextPass;
use Psy\CodeCleaner\ImplicitReturnPass;
use Psy\CodeCleaner\InstanceOfPass;
use Psy\CodeCleaner\LeavePsyshAlonePass;
use Psy\CodeCleaner\LegacyEmptyPass;
use Psy\CodeCleaner\LoopContextPass;
use Psy\CodeCleaner\MagicConstantsPass;
use Psy\CodeCleaner\NamespacePass;
use Psy\CodeCleaner\PassableByReferencePass;
use Psy\CodeCleaner\RequirePass;
use Psy\CodeCleaner\StaticConstructorPass;
use Psy\CodeCleaner\StrictTypesPass;
use Psy\CodeCleaner\UseStatementPass;
use Psy\CodeCleaner\ValidClassNamePass;
use Psy\CodeCleaner\ValidConstantPass;
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 $parser;
private $printer;
private $traverser;
private $namespace;
/**
* CodeCleaner constructor.
*
* @param Parser $parser A PhpParser Parser instance. One will be created if not explicitly supplied
* @param Printer $printer A PhpParser Printer instance. One will be created if not explicitly supplied
* @param NodeTraverser $traverser A PhpParser NodeTraverser instance. One will be created if not explicitly supplied
*/
public function __construct(Parser $parser = null, Printer $printer = null, NodeTraverser $traverser = null)
{
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);
}
}
/**
* Get default CodeCleaner passes.
*
* @return array
*/
private function getDefaultPasses()
{
return array(
// 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 StaticConstructorPass(),
// Rewriting shenanigans
new UseStatementPass(), // must run before the namespace pass
new ExitPass(),
new ImplicitReturnPass(),
new MagicConstantsPass(),
new NamespacePass($this), // 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(),
);
}
/**
* 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 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) === ',');
}
}

View file

@ -0,0 +1,71 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use Psy\Exception\FatalErrorException;
/**
* The abstract class pass handles abstract classes and methods, complaining if there are too few or too many of either.
*/
class AbstractClassPass extends CodeCleanerPass
{
private $class;
private $abstractMethods;
/**
* @throws RuntimeException if the node is an abstract function with a body
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if ($node instanceof Class_) {
$this->class = $node;
$this->abstractMethods = array();
} elseif ($node instanceof ClassMethod) {
if ($node->isAbstract()) {
$name = sprintf('%s::%s', $this->class->name, $node->name);
$this->abstractMethods[] = $name;
if ($node->stmts !== null) {
$msg = sprintf('Abstract function %s cannot contain body', $name);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
}
}
}
/**
* @throws RuntimeException if the node is a non-abstract class with abstract methods
*
* @param Node $node
*/
public function leaveNode(Node $node)
{
if ($node instanceof Class_) {
$count = count($this->abstractMethods);
if ($count > 0 && !$node->isAbstract()) {
$msg = sprintf(
'Class %s contains %d abstract method%s must therefore be declared abstract or implement the remaining methods (%s)',
$node->name,
$count,
($count === 1) ? '' : 's',
implode(', ', $this->abstractMethods)
);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
}
}
}

View file

@ -0,0 +1,39 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\Variable;
use Psy\Exception\FatalErrorException;
/**
* Validate that the user input does not assign the `$this` variable.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
class AssignThisVariablePass extends CodeCleanerPass
{
/**
* Validate that the user input does not assign the `$this` variable.
*
* @throws RuntimeException if the user assign the `$this` variable
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if ($node instanceof Assign && $node->var instanceof Variable && $node->var->name === 'this') {
throw new FatalErrorException('Cannot re-assign $this', 0, E_ERROR, null, $node->getLine());
}
}
}

View file

@ -0,0 +1,54 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use Psy\Exception\FatalErrorException;
/**
* Validate that the user did not use the call-time pass-by-reference that causes a fatal error.
*
* As of PHP 5.4.0, call-time pass-by-reference was removed, so using it will raise a fatal error.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
class CallTimePassByReferencePass extends CodeCleanerPass
{
const EXCEPTION_MESSAGE = 'Call-time pass-by-reference has been removed';
/**
* Validate of use call-time pass-by-reference.
*
* @throws RuntimeException if the user used call-time pass-by-reference in PHP >= 5.4.0
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if (version_compare(PHP_VERSION, '5.4', '<')) {
return;
}
if (!$node instanceof FuncCall && !$node instanceof MethodCall && !$node instanceof StaticCall) {
return;
}
foreach ($node->args as $arg) {
if ($arg->byRef) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
}
}
}
}

View file

@ -0,0 +1,83 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Trait_;
use Psy\Exception\ErrorException;
/**
* The called class pass throws warnings for get_class() and get_called_class()
* outside a class context.
*/
class CalledClassPass extends CodeCleanerPass
{
private $inClass;
/**
* @param array $nodes
*/
public function beforeTraverse(array $nodes)
{
$this->inClass = false;
}
/**
* @throws ErrorException if get_class or get_called_class is called without an object from outside a class
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if ($node instanceof Class_ || $node instanceof Trait_) {
$this->inClass = true;
} elseif ($node instanceof FuncCall && !$this->inClass) {
// We'll give any args at all (besides null) a pass.
// Technically we should be checking whether the args are objects, but this will do for now.
//
// @todo switch this to actually validate args when we get context-aware code cleaner passes.
if (!empty($node->args) && !$this->isNull($node->args[0])) {
return;
}
// We'll ignore name expressions as well (things like `$foo()`)
if (!($node->name instanceof Name)) {
return;
}
$name = strtolower($node->name);
if (in_array($name, array('get_class', 'get_called_class'))) {
$msg = sprintf('%s() called without object from outside a class', $name);
throw new ErrorException($msg, 0, E_USER_WARNING, null, $node->getLine());
}
}
}
/**
* @param Node $node
*/
public function leaveNode(Node $node)
{
if ($node instanceof Class_) {
$this->inClass = false;
}
}
private function isNull(Node $node)
{
return $node->value instanceof ConstFetch && strtolower($node->value->name) === 'null';
}
}

View file

@ -0,0 +1,22 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\NodeVisitorAbstract;
/**
* A CodeCleaner pass is a PhpParser Node Visitor.
*/
abstract class CodeCleanerPass extends NodeVisitorAbstract
{
// Wheee!
}

View file

@ -0,0 +1,32 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr\Exit_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
class ExitPass extends CodeCleanerPass
{
/**
* Converts exit calls to BreakExceptions.
*
* @param \PhpParser\Node $node
*/
public function leaveNode(Node $node)
{
if ($node instanceof Exit_) {
return new StaticCall(new FullyQualifiedName('Psy\Exception\BreakException'), 'exitShell');
}
}
}

View file

@ -0,0 +1,70 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use Psy\Exception\FatalErrorException;
/**
* The final class pass handles final classes.
*/
class FinalClassPass extends CodeCleanerPass
{
private $finalClasses;
/**
* @param array $nodes
*/
public function beforeTraverse(array $nodes)
{
$this->finalClasses = array();
}
/**
* @throws RuntimeException if the node is a class that extends a final class
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if ($node instanceof Class_) {
if ($node->extends) {
$extends = (string) $node->extends;
if ($this->isFinalClass($extends)) {
$msg = sprintf('Class %s may not inherit from final class (%s)', $node->name, $extends);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
}
if ($node->isFinal()) {
$this->finalClasses[strtolower($node->name)] = true;
}
}
}
/**
* @param string $name Class name
*
* @return bool
*/
private function isFinalClass($name)
{
if (!class_exists($name)) {
return isset($this->finalClasses[strtolower($name)]);
}
$refl = new \ReflectionClass($name);
return $refl->isFinal();
}
}

View file

@ -0,0 +1,61 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr\Yield_;
use PhpParser\Node\FunctionLike;
use Psy\Exception\FatalErrorException;
class FunctionContextPass extends CodeCleanerPass
{
/** @var int */
private $functionDepth;
/**
* @param array $nodes
*/
public function beforeTraverse(array $nodes)
{
$this->functionDepth = 0;
}
public function enterNode(Node $node)
{
if ($node instanceof FunctionLike) {
$this->functionDepth++;
return;
}
// node is inside function context
if ($this->functionDepth !== 0) {
return;
}
// It causes fatal error.
if ($node instanceof Yield_) {
$msg = 'The "yield" expression can only be used inside a function';
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
}
/**
* @param \PhpParser\Node $node
*/
public function leaveNode(Node $node)
{
if ($node instanceof FunctionLike) {
$this->functionDepth--;
}
}
}

View file

@ -0,0 +1,81 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\Empty_;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Isset_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Stmt\Unset_;
use Psy\Exception\FatalErrorException;
/**
* Validate that the functions are used correctly.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
class FunctionReturnInWriteContextPass extends CodeCleanerPass
{
const PHP55_MESSAGE = 'Cannot use isset() on the result of a function call (you can use "null !== func()" instead)';
const EXCEPTION_MESSAGE = "Can't use function return value in write context";
private $isPhp55;
public function __construct()
{
$this->isPhp55 = version_compare(PHP_VERSION, '5.5', '>=');
}
/**
* Validate that the functions are used correctly.
*
* @throws FatalErrorException if a function is passed as an argument reference
* @throws FatalErrorException if a function is used as an argument in the isset
* @throws FatalErrorException if a function is used as an argument in the empty, only for PHP < 5.5
* @throws FatalErrorException if a value is assigned to a function
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if ($node instanceof Array_ || $this->isCallNode($node)) {
$items = $node instanceof Array_ ? $node->items : $node->args;
foreach ($items as $item) {
if ($item && $item->byRef && $this->isCallNode($item->value)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
}
}
} elseif ($node instanceof Isset_ || $node instanceof Unset_) {
foreach ($node->vars as $var) {
if (!$this->isCallNode($var)) {
continue;
}
$msg = ($node instanceof Isset_ && $this->isPhp55) ? self::PHP55_MESSAGE : self::EXCEPTION_MESSAGE;
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
} elseif ($node instanceof Empty_ && !$this->isPhp55 && $this->isCallNode($node->expr)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
} elseif ($node instanceof Assign && $this->isCallNode($node->var)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
}
}
private function isCallNode(Node $node)
{
return $node instanceof FuncCall || $node instanceof MethodCall || $node instanceof StaticCall;
}
}

View file

@ -0,0 +1,102 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Exit_;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\Stmt\Switch_;
/**
* Add an implicit "return" to the last statement, provided it can be returned.
*/
class ImplicitReturnPass extends CodeCleanerPass
{
/**
* @param array $nodes
*
* @return array
*/
public function beforeTraverse(array $nodes)
{
return $this->addImplicitReturn($nodes);
}
/**
* @param array $nodes
*
* @return array
*/
private function addImplicitReturn(array $nodes)
{
// If nodes is empty, it can't have a return value.
if (empty($nodes)) {
return array(new Return_(new New_(new FullyQualifiedName('Psy\CodeCleaner\NoReturnValue'))));
}
$last = end($nodes);
// Special case a few types of statements to add an implicit return
// value (even though they technically don't have any return value)
// because showing a return value in these instances is useful and not
// very surprising.
if ($last instanceof If_) {
$last->stmts = $this->addImplicitReturn($last->stmts);
foreach ($last->elseifs as $elseif) {
$elseif->stmts = $this->addImplicitReturn($elseif->stmts);
}
if ($last->else) {
$last->else->stmts = $this->addImplicitReturn($last->else->stmts);
}
} elseif ($last instanceof Switch_) {
foreach ($last->cases as $case) {
// only add an implicit return to cases which end in break
$caseLast = end($case->stmts);
if ($caseLast instanceof Break_) {
$case->stmts = $this->addImplicitReturn(array_slice($case->stmts, 0, -1));
$case->stmts[] = $caseLast;
}
}
} elseif ($last instanceof Expr && !($last instanceof Exit_)) {
$nodes[count($nodes) - 1] = new Return_($last, array(
'startLine' => $last->getLine(),
'endLine' => $last->getLine(),
));
} elseif ($last instanceof Namespace_) {
$last->stmts = $this->addImplicitReturn($last->stmts);
}
// Return a "no return value" for all non-expression statements, so that
// PsySH can suppress the `null` that `eval()` returns otherwise.
//
// Note that statements special cased above (if/elseif/else, switch)
// _might_ implicitly return a value before this catch-all return is
// reached.
//
// We're not adding a fallback return after namespace statements,
// because code outside namespace statements doesn't really work, and
// there's already an implicit return in the namespace statement anyway.
if ($last instanceof Stmt && !$last instanceof Return_ && !$last instanceof Namespace_) {
$nodes[] = new Return_(new New_(new FullyQualifiedName('Psy\CodeCleaner\NoReturnValue')));
}
return $nodes;
}
}

View file

@ -0,0 +1,47 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\Instanceof_;
use PhpParser\Node\Scalar;
use PhpParser\Node\Scalar\Encapsed;
use Psy\Exception\FatalErrorException;
/**
* Validate that the instanceof statement does not receive a scalar value or a non-class constant.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
class InstanceOfPass extends CodeCleanerPass
{
const EXCEPTION_MSG = 'instanceof expects an object instance, constant given';
/**
* Validate that the instanceof statement does not receive a scalar value or a non-class constant.
*
* @throws FatalErrorException if a scalar or a non-class constant is given
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if (!$node instanceof Instanceof_) {
return;
}
if (($node->expr instanceof Scalar && !$node->expr instanceof Encapsed) || $node->expr instanceof ConstFetch) {
throw new FatalErrorException(self::EXCEPTION_MSG, 0, E_ERROR, null, $node->getLine());
}
}
}

View file

@ -0,0 +1,36 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr\Variable;
use Psy\Exception\RuntimeException;
/**
* Validate that the user input does not reference the `$__psysh__` variable.
*/
class LeavePsyshAlonePass extends CodeCleanerPass
{
/**
* Validate that the user input does not reference the `$__psysh__` variable.
*
* @throws RuntimeException if the user is messing with $__psysh__
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if ($node instanceof Variable && $node->name === '__psysh__') {
throw new RuntimeException('Don\'t mess with $__psysh__. Bad things will happen.');
}
}
}

View file

@ -0,0 +1,64 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr\Empty_;
use PhpParser\Node\Expr\Variable;
use Psy\Exception\ParseErrorException;
/**
* Validate that the user did not call the language construct `empty()` on a
* statement in PHP < 5.5.
*/
class LegacyEmptyPass extends CodeCleanerPass
{
/**
* Validate use of empty in PHP < 5.5.
*
* @throws ParseErrorException if the user used empty with anything but a variable
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if (version_compare(PHP_VERSION, '5.5', '>=')) {
return;
}
if (!$node instanceof Empty_) {
return;
}
if (!$node->expr instanceof Variable) {
$msg = sprintf('syntax error, unexpected %s', $this->getUnexpectedThing($node->expr));
throw new ParseErrorException($msg, $node->expr->getLine());
}
}
private function getUnexpectedThing(Node $node)
{
switch ($node->getType()) {
case 'Scalar_String':
case 'Scalar_LNumber':
case 'Scalar_DNumber':
return json_encode($node->value);
case 'Expr_ConstFetch':
return (string) $node->name;
default:
return $node->getType();
}
}
}

View file

@ -0,0 +1,109 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Scalar\DNumber;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\Continue_;
use PhpParser\Node\Stmt\Do_;
use PhpParser\Node\Stmt\For_;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\Node\Stmt\Switch_;
use PhpParser\Node\Stmt\While_;
use Psy\Exception\FatalErrorException;
/**
* The loop context pass handles invalid `break` and `continue` statements.
*/
class LoopContextPass extends CodeCleanerPass
{
private $isPHP54;
private $loopDepth;
public function __construct()
{
$this->isPHP54 = version_compare(PHP_VERSION, '5.4.0', '>=');
}
/**
* {@inheritdoc}
*/
public function beforeTraverse(array $nodes)
{
$this->loopDepth = 0;
}
/**
* @throws FatalErrorException if the node is a break or continue in a non-loop or switch context
* @throws FatalErrorException if the node is trying to break out of more nested structures than exist
* @throws FatalErrorException if the node is a break or continue and has a non-numeric argument
* @throws FatalErrorException if the node is a break or continue and has an argument less than 1
*
* @param Node $node
*/
public function enterNode(Node $node)
{
switch (true) {
case $node instanceof Do_:
case $node instanceof For_:
case $node instanceof Foreach_:
case $node instanceof Switch_:
case $node instanceof While_:
$this->loopDepth++;
break;
case $node instanceof Break_:
case $node instanceof Continue_:
$operator = $node instanceof Break_ ? 'break' : 'continue';
if ($this->loopDepth === 0) {
$msg = sprintf("'%s' not in the 'loop' or 'switch' context", $operator);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
if ($node->num instanceof LNumber || $node->num instanceof DNumber) {
$num = $node->num->value;
if ($this->isPHP54 && ($node->num instanceof DNumber || $num < 1)) {
$msg = sprintf("'%s' operator accepts only positive numbers", $operator);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
if ($num > $this->loopDepth) {
$msg = sprintf("Cannot '%s' %d levels", $operator, $num);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
} elseif ($node->num && $this->isPHP54) {
$msg = sprintf("'%s' operator with non-constant operand is no longer supported", $operator);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
break;
}
}
/**
* @param Node $node
*/
public function leaveNode(Node $node)
{
switch (true) {
case $node instanceof Do_:
case $node instanceof For_:
case $node instanceof Foreach_:
case $node instanceof Switch_:
case $node instanceof While_:
$this->loopDepth--;
break;
}
}
}

View file

@ -0,0 +1,42 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\MagicConst\Dir;
use PhpParser\Node\Scalar\MagicConst\File;
use PhpParser\Node\Scalar\String_;
/**
* Swap out __DIR__ and __FILE__ magic constants with our best guess?
*/
class MagicConstantsPass extends CodeCleanerPass
{
/**
* Swap out __DIR__ and __FILE__ constants, because the default ones when
* calling eval() don't make sense.
*
* @param Node $node
*
* @return null|FuncCall|String_
*/
public function enterNode(Node $node)
{
if ($node instanceof Dir) {
return new FuncCall(new Name('getcwd'), array(), $node->getAttributes());
} elseif ($node instanceof File) {
return new String_('', $node->getAttributes());
}
}
}

View file

@ -0,0 +1,71 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
use PhpParser\Node\Stmt\Namespace_;
/**
* Abstract namespace-aware code cleaner pass.
*/
abstract class NamespaceAwarePass extends CodeCleanerPass
{
protected $namespace;
protected $currentScope;
/**
* @todo should this be final? Extending classes should be sure to either
* use afterTraverse or call parent::beforeTraverse() when overloading.
*
* Reset the namespace and the current scope before beginning analysis
*/
public function beforeTraverse(array $nodes)
{
$this->namespace = array();
$this->currentScope = array();
}
/**
* @todo should this be final? Extending classes should be sure to either use
* leaveNode or call parent::enterNode() when overloading
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if ($node instanceof Namespace_) {
$this->namespace = isset($node->name) ? $node->name->parts : array();
}
}
/**
* Get a fully-qualified name (class, function, interface, etc).
*
* @param mixed $name
*
* @return string
*/
protected function getFullyQualifiedName($name)
{
if ($name instanceof FullyQualifiedName) {
return implode('\\', $name->parts);
} elseif ($name instanceof Name) {
$name = $name->parts;
} elseif (!is_array($name)) {
$name = array($name);
}
return implode('\\', array_merge($this->namespace, $name));
}
}

View file

@ -0,0 +1,77 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Namespace_;
use Psy\CodeCleaner;
/**
* Provide implicit namespaces for subsequent execution.
*
* The namespace pass remembers the last standalone namespace line encountered:
*
* namespace Foo\Bar;
*
* ... which it then applies implicitly to all future evaluated code, until the
* namespace is replaced by another namespace. To reset to the top level
* namespace, enter `namespace {}`. This is a bit ugly, but it does the trick :)
*/
class NamespacePass extends CodeCleanerPass
{
private $namespace = null;
private $cleaner;
/**
* @param CodeCleaner $cleaner
*/
public function __construct(CodeCleaner $cleaner)
{
$this->cleaner = $cleaner;
}
/**
* If this is a standalone namespace line, remember it for later.
*
* Otherwise, apply remembered namespaces to the code until a new namespace
* is encountered.
*
* @param array $nodes
*/
public function beforeTraverse(array $nodes)
{
if (empty($nodes)) {
return $nodes;
}
$last = end($nodes);
if (!$last instanceof Namespace_) {
return $this->namespace ? array(new Namespace_($this->namespace, $nodes)) : $nodes;
}
$this->setNamespace($last->name);
return $nodes;
}
/**
* Remember the namespace and (re)set the namespace on the CodeCleaner as
* well.
*
* @param null|Name $namespace
*/
private function setNamespace($namespace)
{
$this->namespace = $namespace;
$this->cleaner->setNamespace($namespace === null ? null : $namespace->parts);
}
}

View file

@ -0,0 +1,24 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
/**
* A class used internally by CodeCleaner to represent input, such as
* non-expression statements, with no return value.
*
* Note that user code returning an instance of this class will act like it
* has no return value, so you prolly shouldn't do that.
*/
class NoReturnValue
{
// this space intentionally left blank
}

View file

@ -0,0 +1,109 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\Variable;
use Psy\Exception\FatalErrorException;
/**
* Validate that only variables (and variable-like things) are passed by reference.
*/
class PassableByReferencePass extends CodeCleanerPass
{
const EXCEPTION_MESSAGE = 'Only variables can be passed by reference';
/**
* @throws FatalErrorException if non-variables are passed by reference
*
* @param Node $node
*/
public function enterNode(Node $node)
{
// @todo support MethodCall and StaticCall as well.
if ($node instanceof FuncCall) {
// if function name is an expression or a variable, give it a pass for now.
if ($node->name instanceof Expr || $node->name instanceof Variable) {
return;
}
$name = (string) $node->name;
if ($name === 'array_multisort') {
return $this->validateArrayMultisort($node);
}
try {
$refl = new \ReflectionFunction($name);
} catch (\ReflectionException $e) {
// Well, we gave it a shot!
return;
}
foreach ($refl->getParameters() as $key => $param) {
if (array_key_exists($key, $node->args)) {
$arg = $node->args[$key];
if ($param->isPassedByReference() && !$this->isPassableByReference($arg)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
}
}
}
}
}
private function isPassableByReference(Node $arg)
{
// FuncCall, MethodCall and StaticCall are all PHP _warnings_ not fatal errors, so we'll let
// PHP handle those ones :)
return $arg->value instanceof ClassConstFetch ||
$arg->value instanceof PropertyFetch ||
$arg->value instanceof Variable ||
$arg->value instanceof FuncCall ||
$arg->value instanceof MethodCall ||
$arg->value instanceof StaticCall;
}
/**
* Because array_multisort has a problematic signature...
*
* The argument order is all sorts of wonky, and whether something is passed
* by reference or not depends on the values of the two arguments before it.
* We'll do a good faith attempt at validating this, but err on the side of
* permissive.
*
* This is why you don't design languages where core code and extensions can
* implement APIs that wouldn't be possible in userland code.
*
* @throws FatalErrorException for clearly invalid arguments
*
* @param Node $node
*/
private function validateArrayMultisort(Node $node)
{
$nonPassable = 2; // start with 2 because the first one has to be passable by reference
foreach ($node->args as $arg) {
if ($this->isPassableByReference($arg)) {
$nonPassable = 0;
} elseif (++$nonPassable > 2) {
// There can be *at most* two non-passable-by-reference args in a row. This is about
// as close as we can get to validating the arguments for this function :-/
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
}
}
}
}

View file

@ -0,0 +1,102 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Include_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
use PhpParser\Node\Scalar\LNumber;
use Psy\Exception\ErrorException;
use Psy\Exception\FatalErrorException;
use Psy\Shell;
/**
* Add runtime validation for `require` and `require_once` calls.
*/
class RequirePass extends CodeCleanerPass
{
private static $requireTypes = array(Include_::TYPE_REQUIRE, Include_::TYPE_REQUIRE_ONCE);
/**
* {@inheritdoc}
*/
public function enterNode(Node $origNode)
{
if (!$this->isRequireNode($origNode)) {
return;
}
$node = clone $origNode;
/*
* rewrite
*
* $foo = require $bar
*
* to
*
* $foo = require \Psy\CodeCleaner\RequirePass::resolve($bar)
*/
$node->expr = new StaticCall(
new FullyQualifiedName('Psy\CodeCleaner\RequirePass'),
'resolve',
array(new Arg($origNode->expr), new Arg(new LNumber($origNode->getLine()))),
$origNode->getAttributes()
);
return $node;
}
/**
* Runtime validation that $file can be resolved as an include path.
*
* If $file can be resolved, return $file. Otherwise throw a fatal error exception.
*
* @throws FatalErrorException when unable to resolve include path for $file
* @throws ErrorException if $file is empty and E_WARNING is included in error_reporting level
*
* @param string $file
* @param int $lineNumber Line number of the original require expression
*
* @return string Exactly the same as $file
*/
public static function resolve($file, $lineNumber = null)
{
$file = (string) $file;
if ($file === '') {
// @todo Shell::handleError would be better here, because we could
// fake the file and line number, but we can't call it statically.
// So we're duplicating some of the logics here.
if (E_WARNING & error_reporting()) {
ErrorException::throwException(E_WARNING, 'Filename cannot be empty', null, $lineNumber);
} else {
// @todo trigger an error as fallback? this is pretty ugly…
// trigger_error('Filename cannot be empty', E_USER_WARNING);
}
}
if ($file === '' || !stream_resolve_include_path($file)) {
$msg = sprintf("Failed opening required '%s'", $file);
throw new FatalErrorException($msg, 0, E_ERROR, null, $lineNumber);
}
return $file;
}
private function isRequireNode(Node $node)
{
return $node instanceof Include_ && in_array($node->type, self::$requireTypes);
}
}

View file

@ -0,0 +1,88 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Namespace_;
use Psy\Exception\FatalErrorException;
/**
* Validate that the old-style constructor function is not static.
*
* As of PHP 5.3.3, methods with the same name as the last element of a namespaced class name
* will no longer be treated as constructor. This change doesn't affect non-namespaced classes.
*
* Validation of the __construct method ensures the PHP Parser.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
class StaticConstructorPass extends CodeCleanerPass
{
private $isPHP533;
private $namespace;
public function __construct()
{
$this->isPHP533 = version_compare(PHP_VERSION, '5.3.3', '>=');
}
public function beforeTraverse(array $nodes)
{
$this->namespace = array();
}
/**
* Validate that the old-style constructor function is not static.
*
* @throws FatalErrorException if the old-style constructor function is static
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if ($node instanceof Namespace_) {
$this->namespace = isset($node->name) ? $node->name->parts : array();
} elseif ($node instanceof Class_) {
// Bail early if this is PHP 5.3.3 and we have a namespaced class
if (!empty($this->namespace) && $this->isPHP533) {
return;
}
$constructor = null;
foreach ($node->stmts as $stmt) {
if ($stmt instanceof ClassMethod) {
// Bail early if we find a new-style constructor
if ('__construct' === strtolower($stmt->name)) {
return;
}
// We found a possible old-style constructor
// (unless there is also a __construct method)
if (strtolower($node->name) === strtolower($stmt->name)) {
$constructor = $stmt;
}
}
}
if ($constructor && $constructor->isStatic()) {
$msg = sprintf(
'Constructor %s::%s() cannot be static',
implode('\\', array_merge($this->namespace, (array) $node->name)),
$constructor->name
);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
}
}
}

View file

@ -0,0 +1,78 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt\Declare_;
use PhpParser\Node\Stmt\DeclareDeclare;
use Psy\Exception\FatalErrorException;
/**
* Provide implicit strict types declarations for for subsequent execution.
*
* The strict types pass remembers the last strict types declaration:
*
* declare(strict_types=1);
*
* ... which it then applies implicitly to all future evaluated code, until it
* is replaced by a new declaration.
*/
class StrictTypesPass extends CodeCleanerPass
{
const EXCEPTION_MESSAGE = 'strict_types declaration must have 0 or 1 as its value';
private $strictTypes = false;
/**
* If this is a standalone strict types declaration, remember it for later.
*
* Otherwise, apply remembered strict types declaration to to the code until
* a new declaration is encountered.
*
* @throws FatalErrorException if an invalid `strict_types` declaration is found
*
* @param array $nodes
*/
public function beforeTraverse(array $nodes)
{
if (version_compare(PHP_VERSION, '7.0', '<')) {
return;
}
$prependStrictTypes = $this->strictTypes;
foreach ($nodes as $key => $node) {
if ($node instanceof Declare_) {
foreach ($node->declares as $declare) {
if ($declare->key === 'strict_types') {
$value = $declare->value;
if (!$value instanceof LNumber || ($value->value !== 0 && $value->value !== 1)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
}
$this->strictTypes = $value->value === 1;
}
}
}
}
if ($prependStrictTypes) {
$first = reset($nodes);
if (!$first instanceof Declare_) {
$declare = new Declare_(array(new DeclareDeclare('strict_types', new LNumber(1))));
array_unshift($nodes, $declare);
}
}
return $nodes;
}
}

View file

@ -0,0 +1,123 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
use PhpParser\Node\Stmt\GroupUse;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Use_;
/**
* Provide implicit use statements for subsequent execution.
*
* The use statement pass remembers the last use statement line encountered:
*
* use Foo\Bar as Baz;
*
* ... which it then applies implicitly to all future evaluated code, until the
* current namespace is replaced by another namespace.
*/
class UseStatementPass extends CodeCleanerPass
{
private $aliases = array();
private $lastAliases = array();
private $lastNamespace = null;
/**
* Re-load the last set of use statements on re-entering a namespace.
*
* This isn't how namespaces normally work, but because PsySH has to spin
* up a new namespace for every line of code, we do this to make things
* work like you'd expect.
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if ($node instanceof Namespace_) {
// If this is the same namespace as last namespace, let's do ourselves
// a favor and reload all the aliases...
if (strtolower($node->name) === strtolower($this->lastNamespace)) {
$this->aliases = $this->lastAliases;
}
}
}
/**
* If this statement is a namespace, forget all the aliases we had.
*
* If it's a use statement, remember the alias for later. Otherwise, apply
* remembered aliases to the code.
*
* @param Node $node
*/
public function leaveNode(Node $node)
{
if ($node instanceof Use_) {
// Store a reference to every "use" statement, because we'll need
// them in a bit.
foreach ($node->uses as $use) {
$this->aliases[strtolower($use->alias)] = $use->name;
}
return false;
} elseif ($node instanceof GroupUse) {
// Expand every "use" statement in the group into a full, standalone
// "use" and store 'em with the others.
foreach ($node->uses as $use) {
$this->aliases[strtolower($use->alias)] = Name::concat($node->prefix, $use->name, array(
'startLine' => $node->prefix->getAttribute('startLine'),
'endLine' => $use->name->getAttribute('endLine'),
));
}
return false;
} elseif ($node instanceof Namespace_) {
// Start fresh, since we're done with this namespace.
$this->lastNamespace = $node->name;
$this->lastAliases = $this->aliases;
$this->aliases = array();
} else {
foreach ($node as $name => $subNode) {
if ($subNode instanceof Name) {
// Implicitly thunk all aliases.
if ($replacement = $this->findAlias($subNode)) {
$node->$name = $replacement;
}
}
}
return $node;
}
}
/**
* Find class/namespace aliases.
*
* @param Name $name
*
* @return FullyQualifiedName|null
*/
private function findAlias(Name $name)
{
$that = strtolower($name);
foreach ($this->aliases as $alias => $prefix) {
if ($that === $alias) {
return new FullyQualifiedName($prefix->toString());
} elseif (substr($that, 0, strlen($alias) + 1) === $alias . '\\') {
return new FullyQualifiedName($prefix->toString() . substr($name, strlen($alias)));
}
}
}
}

View file

@ -0,0 +1,393 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Do_;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Switch_;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\Node\Stmt\While_;
use Psy\Exception\FatalErrorException;
/**
* Validate that classes exist.
*
* This pass throws a FatalErrorException rather than letting PHP run
* headfirst into a real fatal error and die.
*/
class ValidClassNamePass extends NamespaceAwarePass
{
const CLASS_TYPE = 'class';
const INTERFACE_TYPE = 'interface';
const TRAIT_TYPE = 'trait';
protected $checkTraits;
private $conditionalScopes = 0;
public function __construct()
{
$this->checkTraits = function_exists('trait_exists');
}
/**
* Validate class, interface and trait definitions.
*
* Validate them upon entering the node, so that we know about their
* presence and can validate constant fetches and static calls in class or
* trait methods.
*
* @param Node
*/
public function enterNode(Node $node)
{
parent::enterNode($node);
if (self::isConditional($node)) {
$this->conditionalScopes++;
} else {
// @todo add an "else" here which adds a runtime check for instances where we can't tell
// whether a class is being redefined by static analysis alone.
if ($this->conditionalScopes === 0) {
if ($node instanceof Class_) {
$this->validateClassStatement($node);
} elseif ($node instanceof Interface_) {
$this->validateInterfaceStatement($node);
} elseif ($node instanceof Trait_) {
$this->validateTraitStatement($node);
}
}
}
}
/**
* Validate `new` expressions, class constant fetches, and static calls.
*
* @throws FatalErrorException if a class, interface or trait is referenced which does not exist
* @throws FatalErrorException if a class extends something that is not a class
* @throws FatalErrorException if a class implements something that is not an interface
* @throws FatalErrorException if an interface extends something that is not an interface
* @throws FatalErrorException if a class, interface or trait redefines an existing class, interface or trait name
*
* @param Node $node
*/
public function leaveNode(Node $node)
{
if (self::isConditional($node)) {
$this->conditionalScopes--;
} elseif ($node instanceof New_) {
$this->validateNewExpression($node);
} elseif ($node instanceof ClassConstFetch) {
$this->validateClassConstFetchExpression($node);
} elseif ($node instanceof StaticCall) {
$this->validateStaticCallExpression($node);
}
}
private static function isConditional(Node $node)
{
return $node instanceof If_ ||
$node instanceof While_ ||
$node instanceof Do_ ||
$node instanceof Switch_;
}
/**
* Validate a class definition statement.
*
* @param Class_ $stmt
*/
protected function validateClassStatement(Class_ $stmt)
{
$this->ensureCanDefine($stmt);
if (isset($stmt->extends)) {
$this->ensureClassExists($this->getFullyQualifiedName($stmt->extends), $stmt);
}
$this->ensureInterfacesExist($stmt->implements, $stmt);
}
/**
* Validate an interface definition statement.
*
* @param Interface_ $stmt
*/
protected function validateInterfaceStatement(Interface_ $stmt)
{
$this->ensureCanDefine($stmt);
$this->ensureInterfacesExist($stmt->extends, $stmt);
}
/**
* Validate a trait definition statement.
*
* @param Trait_ $stmt
*/
protected function validateTraitStatement(Trait_ $stmt)
{
$this->ensureCanDefine($stmt);
}
/**
* Validate a `new` expression.
*
* @param New_ $stmt
*/
protected function validateNewExpression(New_ $stmt)
{
// if class name is an expression or an anonymous class, give it a pass for now
if (!$stmt->class instanceof Expr && !$stmt->class instanceof Class_) {
$this->ensureClassExists($this->getFullyQualifiedName($stmt->class), $stmt);
}
}
/**
* Validate a class constant fetch expression's class.
*
* @param ClassConstFetch $stmt
*/
protected function validateClassConstFetchExpression(ClassConstFetch $stmt)
{
// there is no need to check exists for ::class const for php 5.5 or newer
if (strtolower($stmt->name) === 'class'
&& version_compare(PHP_VERSION, '5.5', '>=')) {
return;
}
// if class name is an expression, give it a pass for now
if (!$stmt->class instanceof Expr) {
$this->ensureClassOrInterfaceExists($this->getFullyQualifiedName($stmt->class), $stmt);
}
}
/**
* Validate a class constant fetch expression's class.
*
* @param StaticCall $stmt
*/
protected function validateStaticCallExpression(StaticCall $stmt)
{
// if class name is an expression, give it a pass for now
if (!$stmt->class instanceof Expr) {
$this->ensureMethodExists($this->getFullyQualifiedName($stmt->class), $stmt->name, $stmt);
}
}
/**
* Ensure that no class, interface or trait name collides with a new definition.
*
* @throws FatalErrorException
*
* @param Stmt $stmt
*/
protected function ensureCanDefine(Stmt $stmt)
{
$name = $this->getFullyQualifiedName($stmt->name);
// check for name collisions
$errorType = null;
if ($this->classExists($name)) {
$errorType = self::CLASS_TYPE;
} elseif ($this->interfaceExists($name)) {
$errorType = self::INTERFACE_TYPE;
} elseif ($this->traitExists($name)) {
$errorType = self::TRAIT_TYPE;
}
if ($errorType !== null) {
throw $this->createError(sprintf('%s named %s already exists', ucfirst($errorType), $name), $stmt);
}
// Store creation for the rest of this code snippet so we can find local
// issue too
$this->currentScope[strtolower($name)] = $this->getScopeType($stmt);
}
/**
* Ensure that a referenced class exists.
*
* @throws FatalErrorException
*
* @param string $name
* @param Stmt $stmt
*/
protected function ensureClassExists($name, $stmt)
{
if (!$this->classExists($name)) {
throw $this->createError(sprintf('Class \'%s\' not found', $name), $stmt);
}
}
/**
* Ensure that a referenced class _or interface_ exists.
*
* @throws FatalErrorException
*
* @param string $name
* @param Stmt $stmt
*/
protected function ensureClassOrInterfaceExists($name, $stmt)
{
if (!$this->classExists($name) && !$this->interfaceExists($name)) {
throw $this->createError(sprintf('Class \'%s\' not found', $name), $stmt);
}
}
/**
* Ensure that a statically called method exists.
*
* @throws FatalErrorException
*
* @param string $class
* @param string $name
* @param Stmt $stmt
*/
protected function ensureMethodExists($class, $name, $stmt)
{
$this->ensureClassExists($class, $stmt);
// let's pretend all calls to self, parent and static are valid
if (in_array(strtolower($class), array('self', 'parent', 'static'))) {
return;
}
// ... and all calls to classes defined right now
if ($this->findInScope($class) === self::CLASS_TYPE) {
return;
}
// if method name is an expression, give it a pass for now
if ($name instanceof Expr) {
return;
}
if (!method_exists($class, $name) && !method_exists($class, '__callStatic')) {
throw $this->createError(sprintf('Call to undefined method %s::%s()', $class, $name), $stmt);
}
}
/**
* Ensure that a referenced interface exists.
*
* @throws FatalErrorException
*
* @param $interfaces
* @param Stmt $stmt
*/
protected function ensureInterfacesExist($interfaces, $stmt)
{
foreach ($interfaces as $interface) {
/** @var string $name */
$name = $this->getFullyQualifiedName($interface);
if (!$this->interfaceExists($name)) {
throw $this->createError(sprintf('Interface \'%s\' not found', $name), $stmt);
}
}
}
/**
* Get a symbol type key for storing in the scope name cache.
*
* @param Stmt $stmt
*
* @return string
*/
protected function getScopeType(Stmt $stmt)
{
if ($stmt instanceof Class_) {
return self::CLASS_TYPE;
} elseif ($stmt instanceof Interface_) {
return self::INTERFACE_TYPE;
} elseif ($stmt instanceof Trait_) {
return self::TRAIT_TYPE;
}
}
/**
* Check whether a class exists, or has been defined in the current code snippet.
*
* Gives `self`, `static` and `parent` a free pass.
*
* @param string $name
*
* @return bool
*/
protected function classExists($name)
{
// Give `self`, `static` and `parent` a pass. This will actually let
// some errors through, since we're not checking whether the keyword is
// being used in a class scope.
if (in_array(strtolower($name), array('self', 'static', 'parent'))) {
return true;
}
return class_exists($name) || $this->findInScope($name) === self::CLASS_TYPE;
}
/**
* Check whether an interface exists, or has been defined in the current code snippet.
*
* @param string $name
*
* @return bool
*/
protected function interfaceExists($name)
{
return interface_exists($name) || $this->findInScope($name) === self::INTERFACE_TYPE;
}
/**
* Check whether a trait exists, or has been defined in the current code snippet.
*
* @param string $name
*
* @return bool
*/
protected function traitExists($name)
{
return $this->checkTraits && (trait_exists($name) || $this->findInScope($name) === self::TRAIT_TYPE);
}
/**
* Find a symbol in the current code snippet scope.
*
* @param string $name
*
* @return string|null
*/
protected function findInScope($name)
{
$name = strtolower($name);
if (isset($this->currentScope[$name])) {
return $this->currentScope[$name];
}
}
/**
* Error creation factory.
*
* @param string $msg
* @param Stmt $stmt
*
* @return FatalErrorException
*/
protected function createError($msg, $stmt)
{
return new FatalErrorException($msg, 0, E_ERROR, null, $stmt->getLine());
}
}

View file

@ -0,0 +1,86 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\ConstFetch;
use Psy\Exception\FatalErrorException;
/**
* Validate that namespaced constant references will succeed.
*
* This pass throws a FatalErrorException rather than letting PHP run
* headfirst into a real fatal error and die.
*
* @todo Detect constants defined in the current code snippet?
* ... Might not be worth it, since it would need to both be defining and
* referencing a namespaced constant, which doesn't seem like that big of
* a target for failure
*/
class ValidConstantPass extends NamespaceAwarePass
{
/**
* Validate that namespaced constant references will succeed.
*
* Note that this does not (yet) detect constants defined in the current code
* snippet. It won't happen very often, so we'll punt for now.
*
* @throws FatalErrorException if a constant reference is not defined
*
* @param Node $node
*/
public function leaveNode(Node $node)
{
if ($node instanceof ConstFetch && count($node->name->parts) > 1) {
$name = $this->getFullyQualifiedName($node->name);
if (!defined($name)) {
$msg = sprintf('Undefined constant %s', $name);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
} elseif ($node instanceof ClassConstFetch) {
$this->validateClassConstFetchExpression($node);
}
}
/**
* Validate a class constant fetch expression.
*
* @throws FatalErrorException if a class constant is not defined
*
* @param ClassConstFetch $stmt
*/
protected function validateClassConstFetchExpression(ClassConstFetch $stmt)
{
// give the `class` pseudo-constant a pass
if ($stmt->name === 'class') {
return;
}
// if class name is an expression, give it a pass for now
if (!$stmt->class instanceof Expr) {
$className = $this->getFullyQualifiedName($stmt->class);
// if the class doesn't exist, don't throw an exception… it might be
// defined in the same line it's used or something stupid like that.
if (class_exists($className) || interface_exists($className)) {
$refl = new \ReflectionClass($className);
if (!$refl->hasConstant($stmt->name)) {
$constType = class_exists($className) ? 'Class' : 'Interface';
$msg = sprintf('%s constant \'%s::%s\' not found', $constType, $className, $stmt->name);
throw new FatalErrorException($msg, 0, E_ERROR, null, $stmt->getLine());
}
}
}
}
}

View file

@ -0,0 +1,97 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt\Do_;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\Switch_;
use PhpParser\Node\Stmt\While_;
use Psy\Exception\FatalErrorException;
/**
* Validate that function calls will succeed.
*
* This pass throws a FatalErrorException rather than letting PHP run
* headfirst into a real fatal error and die.
*/
class ValidFunctionNamePass extends NamespaceAwarePass
{
private $conditionalScopes = 0;
/**
* Store newly defined function names on the way in, to allow recursion.
*
* @param Node $node
*/
public function enterNode(Node $node)
{
parent::enterNode($node);
if (self::isConditional($node)) {
$this->conditionalScopes++;
} elseif ($node instanceof Function_) {
$name = $this->getFullyQualifiedName($node->name);
// @todo add an "else" here which adds a runtime check for instances where we can't tell
// whether a function is being redefined by static analysis alone.
if ($this->conditionalScopes === 0) {
if (function_exists($name) ||
isset($this->currentScope[strtolower($name)])) {
$msg = sprintf('Cannot redeclare %s()', $name);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
}
$this->currentScope[strtolower($name)] = true;
}
}
/**
* Validate that function calls will succeed.
*
* @throws FatalErrorException if a function is redefined
* @throws FatalErrorException if the function name is a string (not an expression) and is not defined
*
* @param Node $node
*/
public function leaveNode(Node $node)
{
if (self::isConditional($node)) {
$this->conditionalScopes--;
} elseif ($node instanceof FuncCall) {
// if function name is an expression or a variable, give it a pass for now.
$name = $node->name;
if (!$name instanceof Expr && !$name instanceof Variable) {
$shortName = implode('\\', $name->parts);
$fullName = $this->getFullyQualifiedName($name);
$inScope = isset($this->currentScope[strtolower($fullName)]);
if (!$inScope && !function_exists($shortName) && !function_exists($fullName)) {
$message = sprintf('Call to undefined function %s()', $name);
throw new FatalErrorException($message, 0, E_ERROR, null, $node->getLine());
}
}
}
}
private static function isConditional(Node $node)
{
return $node instanceof If_ ||
$node instanceof While_ ||
$node instanceof Do_ ||
$node instanceof Switch_;
}
}

View file

@ -0,0 +1,77 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Psy\Output\ShellOutput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Interact with the current code buffer.
*
* Shows and clears the buffer for the current multi-line expression.
*/
class BufferCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('buffer')
->setAliases(array('buf'))
->setDefinition(array(
new InputOption('clear', '', InputOption::VALUE_NONE, 'Clear the current buffer.'),
))
->setDescription('Show (or clear) the contents of the code input buffer.')
->setHelp(
<<<'HELP'
Show the contents of the code buffer for the current multi-line expression.
Optionally, clear the buffer by passing the <info>--clear</info> option.
HELP
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$buf = $this->getApplication()->getCodeBuffer();
if ($input->getOption('clear')) {
$this->getApplication()->resetCodeBuffer();
$output->writeln($this->formatLines($buf, 'urgent'), ShellOutput::NUMBER_LINES);
} else {
$output->writeln($this->formatLines($buf), ShellOutput::NUMBER_LINES);
}
}
/**
* A helper method for wrapping buffer lines in `<urgent>` and `<return>` formatter strings.
*
* @param array $lines
* @param string $type (default: 'return')
*
* @return array Formatted strings
*/
protected function formatLines(array $lines, $type = 'return')
{
$template = sprintf('<%s>%%s</%s>', $type, $type);
return array_map(function ($line) use ($template) {
return sprintf($template, $line);
}, $lines);
}
}

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Clear the Psy Shell.
*
* Just what it says on the tin.
*/
class ClearCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('clear')
->setDefinition(array())
->setDescription('Clear the Psy Shell screen.')
->setHelp(
<<<'HELP'
Clear the Psy Shell screen.
Pro Tip: If your PHP has readline support, you should be able to use ctrl+l too!
HELP
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->write(sprintf('%c[2J%c[0;0f', 27, 27));
}
}

View file

@ -0,0 +1,282 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Psy\Shell;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command as BaseCommand;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableHelper;
use Symfony\Component\Console\Helper\TableStyle;
use Symfony\Component\Console\Output\OutputInterface;
/**
* The Psy Shell base command.
*/
abstract class Command extends BaseCommand
{
/**
* Sets the application instance for this command.
*
* @param Application $application An Application instance
*
* @api
*/
public function setApplication(Application $application = null)
{
if ($application !== null && !$application instanceof Shell) {
throw new \InvalidArgumentException('PsySH Commands require an instance of Psy\Shell.');
}
return parent::setApplication($application);
}
/**
* {@inheritdoc}
*/
public function asText()
{
$messages = array(
'<comment>Usage:</comment>',
' ' . $this->getSynopsis(),
'',
);
if ($this->getAliases()) {
$messages[] = $this->aliasesAsText();
}
if ($this->getArguments()) {
$messages[] = $this->argumentsAsText();
}
if ($this->getOptions()) {
$messages[] = $this->optionsAsText();
}
if ($help = $this->getProcessedHelp()) {
$messages[] = '<comment>Help:</comment>';
$messages[] = ' ' . str_replace("\n", "\n ", $help) . "\n";
}
return implode("\n", $messages);
}
/**
* {@inheritdoc}
*/
private function getArguments()
{
$hidden = $this->getHiddenArguments();
return array_filter($this->getNativeDefinition()->getArguments(), function ($argument) use ($hidden) {
return !in_array($argument->getName(), $hidden);
});
}
/**
* These arguments will be excluded from help output.
*
* @return array
*/
protected function getHiddenArguments()
{
return array('command');
}
/**
* {@inheritdoc}
*/
private function getOptions()
{
$hidden = $this->getHiddenOptions();
return array_filter($this->getNativeDefinition()->getOptions(), function ($option) use ($hidden) {
return !in_array($option->getName(), $hidden);
});
}
/**
* These options will be excluded from help output.
*
* @return array
*/
protected function getHiddenOptions()
{
return array('verbose');
}
/**
* Format command aliases as text..
*
* @return string
*/
private function aliasesAsText()
{
return '<comment>Aliases:</comment> <info>' . implode(', ', $this->getAliases()) . '</info>' . PHP_EOL;
}
/**
* Format command arguments as text.
*
* @return string
*/
private function argumentsAsText()
{
$max = $this->getMaxWidth();
$messages = array();
$arguments = $this->getArguments();
if (!empty($arguments)) {
$messages[] = '<comment>Arguments:</comment>';
foreach ($arguments as $argument) {
if (null !== $argument->getDefault() && (!is_array($argument->getDefault()) || count($argument->getDefault()))) {
$default = sprintf('<comment> (default: %s)</comment>', $this->formatDefaultValue($argument->getDefault()));
} else {
$default = '';
}
$description = str_replace("\n", "\n" . str_pad('', $max + 2, ' '), $argument->getDescription());
$messages[] = sprintf(" <info>%-${max}s</info> %s%s", $argument->getName(), $description, $default);
}
$messages[] = '';
}
return implode(PHP_EOL, $messages);
}
/**
* Format options as text.
*
* @return string
*/
private function optionsAsText()
{
$max = $this->getMaxWidth();
$messages = array();
$options = $this->getOptions();
if ($options) {
$messages[] = '<comment>Options:</comment>';
foreach ($options as $option) {
if ($option->acceptValue() && null !== $option->getDefault() && (!is_array($option->getDefault()) || count($option->getDefault()))) {
$default = sprintf('<comment> (default: %s)</comment>', $this->formatDefaultValue($option->getDefault()));
} else {
$default = '';
}
$multiple = $option->isArray() ? '<comment> (multiple values allowed)</comment>' : '';
$description = str_replace("\n", "\n" . str_pad('', $max + 2, ' '), $option->getDescription());
$optionMax = $max - strlen($option->getName()) - 2;
$messages[] = sprintf(
" <info>%s</info> %-${optionMax}s%s%s%s",
'--' . $option->getName(),
$option->getShortcut() ? sprintf('(-%s) ', $option->getShortcut()) : '',
$description,
$default,
$multiple
);
}
$messages[] = '';
}
return implode(PHP_EOL, $messages);
}
/**
* Calculate the maximum padding width for a set of lines.
*
* @return int
*/
private function getMaxWidth()
{
$max = 0;
foreach ($this->getOptions() as $option) {
$nameLength = strlen($option->getName()) + 2;
if ($option->getShortcut()) {
$nameLength += strlen($option->getShortcut()) + 3;
}
$max = max($max, $nameLength);
}
foreach ($this->getArguments() as $argument) {
$max = max($max, strlen($argument->getName()));
}
return ++$max;
}
/**
* Format an option default as text.
*
* @param mixed $default
*
* @return string
*/
private function formatDefaultValue($default)
{
if (is_array($default) && $default === array_values($default)) {
return sprintf("array('%s')", implode("', '", $default));
}
return str_replace("\n", '', var_export($default, true));
}
/**
* Get a Table instance.
*
* Falls back to legacy TableHelper.
*
* @return Table|TableHelper
*/
protected function getTable(OutputInterface $output)
{
if (!class_exists('Symfony\Component\Console\Helper\Table')) {
return $this->getTableHelper();
}
$style = new TableStyle();
$style
->setVerticalBorderChar(' ')
->setHorizontalBorderChar('')
->setCrossingChar('');
$table = new Table($output);
return $table
->setRows(array())
->setStyle($style);
}
/**
* Legacy fallback for getTable.
*
* @return TableHelper
*/
protected function getTableHelper()
{
$table = $this->getApplication()->getHelperSet()->get('table');
return $table
->setRows(array())
->setLayout(TableHelper::LAYOUT_BORDERLESS)
->setHorizontalBorderChar('')
->setCrossingChar('');
}
}

View file

@ -0,0 +1,119 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Psy\Formatter\DocblockFormatter;
use Psy\Formatter\SignatureFormatter;
use Psy\Reflection\ReflectionLanguageConstruct;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Read the documentation for an object, class, constant, method or property.
*/
class DocCommand extends ReflectingCommand
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('doc')
->setAliases(array('rtfm', 'man'))
->setDefinition(array(
new InputArgument('value', InputArgument::REQUIRED, 'Function, class, instance, constant, method or property to document.'),
))
->setDescription('Read the documentation for an object, class, constant, method or property.')
->setHelp(
<<<HELP
Read the documentation for an object, class, constant, method or property.
It's awesome for well-documented code, not quite as awesome for poorly documented code.
e.g.
<return>>>> doc preg_replace</return>
<return>>>> doc Psy\Shell</return>
<return>>>> doc Psy\Shell::debug</return>
<return>>>> \$s = new Psy\Shell</return>
<return>>>> doc \$s->run</return>
HELP
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$value = $input->getArgument('value');
if (ReflectionLanguageConstruct::isLanguageConstruct($value)) {
$reflector = new ReflectionLanguageConstruct($value);
$doc = $this->getManualDocById($value);
} else {
list($target, $reflector) = $this->getTargetAndReflector($value);
$doc = $this->getManualDoc($reflector) ?: DocblockFormatter::format($reflector);
}
$db = $this->getApplication()->getManualDb();
$output->page(function ($output) use ($reflector, $doc, $db) {
$output->writeln(SignatureFormatter::format($reflector));
$output->writeln('');
if (empty($doc) && !$db) {
$output->writeln('<warning>PHP manual not found</warning>');
$output->writeln(' To document core PHP functionality, download the PHP reference manual:');
$output->writeln(' https://github.com/bobthecow/psysh/wiki/PHP-manual');
} else {
$output->writeln($doc);
}
});
// Set some magic local variables
$this->setCommandScopeVariables($reflector);
}
private function getManualDoc($reflector)
{
switch (get_class($reflector)) {
case 'ReflectionClass':
case 'ReflectionObject':
case 'ReflectionFunction':
$id = $reflector->name;
break;
case 'ReflectionMethod':
$id = $reflector->class . '::' . $reflector->name;
break;
case 'ReflectionProperty':
$id = $reflector->class . '::$' . $reflector->name;
break;
default:
return false;
}
return $this->getManualDocById($id);
}
private function getManualDocById($id)
{
if ($db = $this->getApplication()->getManualDb()) {
return $db
->query(sprintf('SELECT doc FROM php_manual WHERE id = %s', $db->quote($id)))
->fetchColumn(0);
}
}
}

View file

@ -0,0 +1,105 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Psy\Exception\RuntimeException;
use Psy\VarDumper\Presenter;
use Psy\VarDumper\PresenterAware;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Dump an object or primitive.
*
* This is like var_dump but *way* awesomer.
*/
class DumpCommand extends ReflectingCommand implements PresenterAware
{
private $presenter;
/**
* PresenterAware interface.
*
* @param Presenter $presenter
*/
public function setPresenter(Presenter $presenter)
{
$this->presenter = $presenter;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('dump')
->setDefinition(array(
new InputArgument('target', InputArgument::REQUIRED, 'A target object or primitive to dump.', null),
new InputOption('depth', '', InputOption::VALUE_REQUIRED, 'Depth to parse', 10),
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Include private and protected methods and properties.'),
))
->setDescription('Dump an object or primitive.')
->setHelp(
<<<'HELP'
Dump an object or primitive.
This is like var_dump but <strong>way</strong> awesomer.
e.g.
<return>>>> dump $_</return>
<return>>>> dump $someVar</return>
HELP
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$depth = $input->getOption('depth');
$target = $this->resolveTarget($input->getArgument('target'));
$output->page($this->presenter->present($target, $depth, $input->getOption('all') ? Presenter::VERBOSE : 0));
if (is_object($target)) {
$this->setCommandScopeVariables(new \ReflectionObject($target));
}
}
/**
* Resolve dump target name.
*
* @throws RuntimeException if target name does not exist in the current scope
*
* @param string $target
*
* @return mixed
*/
protected function resolveTarget($target)
{
$matches = array();
if (preg_match(self::SUPERGLOBAL, $target, $matches)) {
if (!array_key_exists($matches[1], $GLOBALS)) {
throw new RuntimeException('Unknown target: ' . $target);
}
return $GLOBALS[$matches[1]];
} elseif (preg_match(self::INSTANCE, $target, $matches)) {
return $this->getScopeVariable($matches[1]);
} else {
throw new RuntimeException('Unknown target: ' . $target);
}
}
}

View file

@ -0,0 +1,187 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Psy\Context;
use Psy\ContextAware;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class EditCommand extends Command implements ContextAware
{
/**
* @var string
*/
private $runtimeDir = '';
/**
* @var Context
*/
private $context;
/**
* Constructor.
*
* @param string $runtimeDir The directory to use for temporary files
* @param string|null $name The name of the command; passing null means it must be set in configure()
*
* @throws \Symfony\Component\Console\Exception\LogicException When the command name is empty
*/
public function __construct($runtimeDir, $name = null)
{
parent::__construct($name);
$this->runtimeDir = $runtimeDir;
}
protected function configure()
{
$this
->setName('edit')
->setDefinition(array(
new InputArgument('file', InputArgument::OPTIONAL, 'The file to open for editing. If this is not given, edits a temporary file.', null),
new InputOption(
'exec',
'e',
InputOption::VALUE_NONE,
'Execute the file content after editing. This is the default when a file name argument is not given.',
null
),
new InputOption(
'no-exec',
'E',
InputOption::VALUE_NONE,
'Do not execute the file content after editing. This is the default when a file name argument is given.',
null
),
))
->setDescription('Open an external editor. Afterwards, get produced code in input buffer.')
->setHelp('Set the EDITOR environment variable to something you\'d like to use.');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
*
* @throws \InvalidArgumentException when both exec and no-exec flags are given or if a given variable is not found in the current context
* @throws \UnexpectedValueException if file_get_contents on the edited file returns false instead of a string
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
if ($input->getOption('exec') &&
$input->getOption('no-exec')) {
throw new \InvalidArgumentException('The --exec and --no-exec flags are mutually exclusive.');
}
$filePath = $this->extractFilePath($input->getArgument('file'));
$execute = $this->shouldExecuteFile(
$input->getOption('exec'),
$input->getOption('no-exec'),
$filePath
);
$shouldRemoveFile = false;
if ($filePath === null) {
$filePath = tempnam($this->runtimeDir, 'psysh-edit-command');
$shouldRemoveFile = true;
}
$editedContent = $this->editFile($filePath, $shouldRemoveFile);
if ($execute) {
$this->getApplication()->addInput($editedContent);
}
}
/**
* @param bool $execOption
* @param bool $noExecOption
* @param string|null $filePath
*
* @return bool
*/
private function shouldExecuteFile($execOption, $noExecOption, $filePath)
{
if ($execOption) {
return true;
}
if ($noExecOption) {
return false;
}
// By default, code that is edited is executed if there was no given input file path
return $filePath === null;
}
/**
* @param string|null $fileArgument
*
* @return string|null The file path to edit, null if the input was null, or the value of the referenced variable
*
* @throws \InvalidArgumentException If the variable is not found in the current context
*/
private function extractFilePath($fileArgument)
{
// If the file argument was a variable, get it from the context
if ($fileArgument !== null &&
strlen($fileArgument) > 0 &&
$fileArgument[0] === '$') {
$fileArgument = $this->context->get(preg_replace('/^\$/', '', $fileArgument));
}
return $fileArgument;
}
/**
* @param string $filePath
* @param string $shouldRemoveFile
*
* @return string
*
* @throws \UnexpectedValueException if file_get_contents on $filePath returns false instead of a string
*/
private function editFile($filePath, $shouldRemoveFile)
{
$escapedFilePath = escapeshellarg($filePath);
$pipes = array();
$proc = proc_open((getenv('EDITOR') ?: 'nano') . " {$escapedFilePath}", array(STDIN, STDOUT, STDERR), $pipes);
proc_close($proc);
$editedContent = @file_get_contents($filePath);
if ($shouldRemoveFile) {
@unlink($filePath);
}
if ($editedContent === false) {
throw new \UnexpectedValueException("Reading {$filePath} returned false");
}
return $editedContent;
}
/**
* Set the Context reference.
*
* @param Context $context
*/
public function setContext(Context $context)
{
$this->context = $context;
}
}

View file

@ -0,0 +1,52 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Psy\Exception\BreakException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Exit the Psy Shell.
*
* Just what it says on the tin.
*/
class ExitCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('exit')
->setAliases(array('quit', 'q'))
->setDefinition(array())
->setDescription('End the current session and return to caller.')
->setHelp(
<<<'HELP'
End the current session and return to caller.
e.g.
<return>>>> exit</return>
HELP
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
throw new BreakException('Goodbye.');
}
}

View file

@ -0,0 +1,98 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Symfony\Component\Console\Helper\TableHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Help command.
*
* Lists available commands, and gives command-specific help when asked nicely.
*/
class HelpCommand extends Command
{
private $command;
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('help')
->setAliases(array('?'))
->setDefinition(array(
new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', null),
))
->setDescription('Show a list of commands. Type `help [foo]` for information about [foo].')
->setHelp('My. How meta.');
}
/**
* Helper for setting a subcommand to retrieve help for.
*
* @param Command $command
*/
public function setCommand($command)
{
$this->command = $command;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
if ($this->command !== null) {
// help for an individual command
$output->page($this->command->asText());
$this->command = null;
} elseif ($name = $input->getArgument('command_name')) {
// help for an individual command
$output->page($this->getApplication()->get($name)->asText());
} else {
// list available commands
$commands = $this->getApplication()->all();
$table = $this->getTable($output);
foreach ($commands as $name => $command) {
if ($name !== $command->getName()) {
continue;
}
if ($command->getAliases()) {
$aliases = sprintf('<comment>Aliases:</comment> %s', implode(', ', $command->getAliases()));
} else {
$aliases = '';
}
$table->addRow(array(
sprintf('<info>%s</info>', $name),
$command->getDescription(),
$aliases,
));
}
$output->startPaging();
if ($table instanceof TableHelper) {
$table->render($output);
} else {
$table->render();
}
$output->stopPaging();
}
}
}

View file

@ -0,0 +1,245 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Psy\Input\FilterOptions;
use Psy\Output\ShellOutput;
use Psy\Readline\Readline;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Psy Shell history command.
*
* Shows, searches and replays readline history. Not too shabby.
*/
class HistoryCommand extends Command
{
private $filter;
/**
* {@inheritdoc}
*/
public function __construct($name = null)
{
$this->filter = new FilterOptions();
parent::__construct($name);
}
/**
* Set the Shell's Readline service.
*
* @param Readline $readline
*/
public function setReadline(Readline $readline)
{
$this->readline = $readline;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
list($grep, $insensitive, $invert) = FilterOptions::getOptions();
$this
->setName('history')
->setAliases(array('hist'))
->setDefinition(array(
new InputOption('show', 's', InputOption::VALUE_REQUIRED, 'Show the given range of lines'),
new InputOption('head', 'H', InputOption::VALUE_REQUIRED, 'Display the first N items.'),
new InputOption('tail', 'T', InputOption::VALUE_REQUIRED, 'Display the last N items.'),
$grep,
$insensitive,
$invert,
new InputOption('no-numbers', 'N', InputOption::VALUE_NONE, 'Omit line numbers.'),
new InputOption('save', '', InputOption::VALUE_REQUIRED, 'Save history to a file.'),
new InputOption('replay', '', InputOption::VALUE_NONE, 'Replay'),
new InputOption('clear', '', InputOption::VALUE_NONE, 'Clear the history.'),
))
->setDescription('Show the Psy Shell history.')
->setHelp(
<<<'HELP'
Show, search, save or replay the Psy Shell history.
e.g.
<return>>>> history --grep /[bB]acon/</return>
<return>>>> history --show 0..10 --replay</return>
<return>>>> history --clear</return>
<return>>>> history --tail 1000 --save somefile.txt</return>
HELP
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->validateOnlyOne($input, array('show', 'head', 'tail'));
$this->validateOnlyOne($input, array('save', 'replay', 'clear'));
$history = $this->getHistorySlice(
$input->getOption('show'),
$input->getOption('head'),
$input->getOption('tail')
);
$highlighted = false;
$this->filter->bind($input);
if ($this->filter->hasFilter()) {
$matches = array();
$highlighted = array();
foreach ($history as $i => $line) {
if ($this->filter->match($line, $matches)) {
if (isset($matches[0])) {
$chunks = explode($matches[0], $history[$i]);
$chunks = array_map(array(__CLASS__, 'escape'), $chunks);
$glue = sprintf('<urgent>%s</urgent>', self::escape($matches[0]));
$highlighted[$i] = implode($glue, $chunks);
}
} else {
unset($history[$i]);
}
}
}
if ($save = $input->getOption('save')) {
$output->writeln(sprintf('Saving history in %s...', $save));
file_put_contents($save, implode(PHP_EOL, $history) . PHP_EOL);
$output->writeln('<info>History saved.</info>');
} elseif ($input->getOption('replay')) {
if (!($input->getOption('show') || $input->getOption('head') || $input->getOption('tail'))) {
throw new \InvalidArgumentException('You must limit history via --head, --tail or --show before replaying.');
}
$count = count($history);
$output->writeln(sprintf('Replaying %d line%s of history', $count, ($count !== 1) ? 's' : ''));
$this->getApplication()->addInput($history);
} elseif ($input->getOption('clear')) {
$this->clearHistory();
$output->writeln('<info>History cleared.</info>');
} else {
$type = $input->getOption('no-numbers') ? 0 : ShellOutput::NUMBER_LINES;
if (!$highlighted) {
$type = $type | ShellOutput::OUTPUT_RAW;
}
$output->page($highlighted ?: $history, $type);
}
}
/**
* Extract a range from a string.
*
* @param string $range
*
* @return array [ start, end ]
*/
private function extractRange($range)
{
if (preg_match('/^\d+$/', $range)) {
return array($range, $range + 1);
}
$matches = array();
if ($range !== '..' && preg_match('/^(\d*)\.\.(\d*)$/', $range, $matches)) {
$start = $matches[1] ? intval($matches[1]) : 0;
$end = $matches[2] ? intval($matches[2]) + 1 : PHP_INT_MAX;
return array($start, $end);
}
throw new \InvalidArgumentException('Unexpected range: ' . $range);
}
/**
* Retrieve a slice of the readline history.
*
* @param string $show
* @param string $head
* @param string $tail
*
* @return array A slilce of history
*/
private function getHistorySlice($show, $head, $tail)
{
$history = $this->readline->listHistory();
// don't show the current `history` invocation
array_pop($history);
if ($show) {
list($start, $end) = $this->extractRange($show);
$length = $end - $start;
} elseif ($head) {
if (!preg_match('/^\d+$/', $head)) {
throw new \InvalidArgumentException('Please specify an integer argument for --head.');
}
$start = 0;
$length = intval($head);
} elseif ($tail) {
if (!preg_match('/^\d+$/', $tail)) {
throw new \InvalidArgumentException('Please specify an integer argument for --tail.');
}
$start = count($history) - $tail;
$length = intval($tail) + 1;
} else {
return $history;
}
return array_slice($history, $start, $length, true);
}
/**
* Validate that only one of the given $options is set.
*
* @param InputInterface $input
* @param array $options
*/
private function validateOnlyOne(InputInterface $input, array $options)
{
$count = 0;
foreach ($options as $opt) {
if ($input->getOption($opt)) {
$count++;
}
}
if ($count > 1) {
throw new \InvalidArgumentException('Please specify only one of --' . implode(', --', $options));
}
}
/**
* Clear the readline history.
*/
private function clearHistory()
{
$this->readline->clearHistory();
}
public static function escape($string)
{
return OutputFormatter::escape($string);
}
}

View file

@ -0,0 +1,275 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Psy\Command\ListCommand\ClassConstantEnumerator;
use Psy\Command\ListCommand\ClassEnumerator;
use Psy\Command\ListCommand\ConstantEnumerator;
use Psy\Command\ListCommand\FunctionEnumerator;
use Psy\Command\ListCommand\GlobalVariableEnumerator;
use Psy\Command\ListCommand\MethodEnumerator;
use Psy\Command\ListCommand\PropertyEnumerator;
use Psy\Command\ListCommand\VariableEnumerator;
use Psy\Exception\RuntimeException;
use Psy\Input\FilterOptions;
use Psy\VarDumper\Presenter;
use Psy\VarDumper\PresenterAware;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\TableHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* List available local variables, object properties, etc.
*/
class ListCommand extends ReflectingCommand implements PresenterAware
{
protected $presenter;
protected $enumerators;
/**
* PresenterAware interface.
*
* @param Presenter $manager
*/
public function setPresenter(Presenter $presenter)
{
$this->presenter = $presenter;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
list($grep, $insensitive, $invert) = FilterOptions::getOptions();
$this
->setName('ls')
->setAliases(array('list', 'dir'))
->setDefinition(array(
new InputArgument('target', InputArgument::OPTIONAL, 'A target class or object to list.', null),
new InputOption('vars', '', InputOption::VALUE_NONE, 'Display variables.'),
new InputOption('constants', 'c', InputOption::VALUE_NONE, 'Display defined constants.'),
new InputOption('functions', 'f', InputOption::VALUE_NONE, 'Display defined functions.'),
new InputOption('classes', 'k', InputOption::VALUE_NONE, 'Display declared classes.'),
new InputOption('interfaces', 'I', InputOption::VALUE_NONE, 'Display declared interfaces.'),
new InputOption('traits', 't', InputOption::VALUE_NONE, 'Display declared traits.'),
new InputOption('no-inherit', '', InputOption::VALUE_NONE, 'Exclude inherited methods, properties and constants.'),
new InputOption('properties', 'p', InputOption::VALUE_NONE, 'Display class or object properties (public properties by default).'),
new InputOption('methods', 'm', InputOption::VALUE_NONE, 'Display class or object methods (public methods by default).'),
$grep,
$insensitive,
$invert,
new InputOption('globals', 'g', InputOption::VALUE_NONE, 'Include global variables.'),
new InputOption('internal', 'n', InputOption::VALUE_NONE, 'Limit to internal functions and classes.'),
new InputOption('user', 'u', InputOption::VALUE_NONE, 'Limit to user-defined constants, functions and classes.'),
new InputOption('category', 'C', InputOption::VALUE_REQUIRED, 'Limit to constants in a specific category (e.g. "date").'),
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Include private and protected methods and properties.'),
new InputOption('long', 'l', InputOption::VALUE_NONE, 'List in long format: includes class names and method signatures.'),
))
->setDescription('List local, instance or class variables, methods and constants.')
->setHelp(
<<<'HELP'
List variables, constants, classes, interfaces, traits, functions, methods,
and properties.
Called without options, this will return a list of variables currently in scope.
If a target object is provided, list properties, constants and methods of that
target. If a class, interface or trait name is passed instead, list constants
and methods on that class.
e.g.
<return>>>> ls</return>
<return>>>> ls $foo</return>
<return>>>> ls -k --grep mongo -i</return>
<return>>>> ls -al ReflectionClass</return>
<return>>>> ls --constants --category date</return>
<return>>>> ls -l --functions --grep /^array_.*/</return>
HELP
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->validateInput($input);
$this->initEnumerators();
$method = $input->getOption('long') ? 'writeLong' : 'write';
if ($target = $input->getArgument('target')) {
list($target, $reflector) = $this->getTargetAndReflector($target, true);
} else {
$reflector = null;
}
// @todo something cleaner than this :-/
if ($input->getOption('long')) {
$output->startPaging();
}
foreach ($this->enumerators as $enumerator) {
$this->$method($output, $enumerator->enumerate($input, $reflector, $target));
}
if ($input->getOption('long')) {
$output->stopPaging();
}
// Set some magic local variables
if ($reflector !== null) {
$this->setCommandScopeVariables($reflector);
}
}
/**
* Initialize Enumerators.
*/
protected function initEnumerators()
{
if (!isset($this->enumerators)) {
$mgr = $this->presenter;
$this->enumerators = array(
new ClassConstantEnumerator($mgr),
new ClassEnumerator($mgr),
new ConstantEnumerator($mgr),
new FunctionEnumerator($mgr),
new GlobalVariableEnumerator($mgr),
new PropertyEnumerator($mgr),
new MethodEnumerator($mgr),
new VariableEnumerator($mgr, $this->context),
);
}
}
/**
* Write the list items to $output.
*
* @param OutputInterface $output
* @param null|array $result List of enumerated items
*/
protected function write(OutputInterface $output, array $result = null)
{
if ($result === null) {
return;
}
foreach ($result as $label => $items) {
$names = array_map(array($this, 'formatItemName'), $items);
$output->writeln(sprintf('<strong>%s</strong>: %s', $label, implode(', ', $names)));
}
}
/**
* Write the list items to $output.
*
* Items are listed one per line, and include the item signature.
*
* @param OutputInterface $output
* @param null|array $result List of enumerated items
*/
protected function writeLong(OutputInterface $output, array $result = null)
{
if ($result === null) {
return;
}
$table = $this->getTable($output);
foreach ($result as $label => $items) {
$output->writeln('');
$output->writeln(sprintf('<strong>%s:</strong>', $label));
$table->setRows(array());
foreach ($items as $item) {
$table->addRow(array($this->formatItemName($item), $item['value']));
}
if ($table instanceof TableHelper) {
$table->render($output);
} else {
$table->render();
}
}
}
/**
* Format an item name given its visibility.
*
* @param array $item
*
* @return string
*/
private function formatItemName($item)
{
return sprintf('<%s>%s</%s>', $item['style'], OutputFormatter::escape($item['name']), $item['style']);
}
/**
* Validate that input options make sense, provide defaults when called without options.
*
* @throws RuntimeException if options are inconsistent
*
* @param InputInterface $input
*/
private function validateInput(InputInterface $input)
{
if (!$input->getArgument('target')) {
// if no target is passed, there can be no properties or methods
foreach (array('properties', 'methods', 'no-inherit') as $option) {
if ($input->getOption($option)) {
throw new RuntimeException('--' . $option . ' does not make sense without a specified target.');
}
}
foreach (array('globals', 'vars', 'constants', 'functions', 'classes', 'interfaces', 'traits') as $option) {
if ($input->getOption($option)) {
return;
}
}
// default to --vars if no other options are passed
$input->setOption('vars', true);
} else {
// if a target is passed, classes, functions, etc don't make sense
foreach (array('vars', 'globals', 'functions', 'classes', 'interfaces', 'traits') as $option) {
if ($input->getOption($option)) {
throw new RuntimeException('--' . $option . ' does not make sense with a specified target.');
}
}
foreach (array('constants', 'properties', 'methods') as $option) {
if ($input->getOption($option)) {
return;
}
}
// default to --constants --properties --methods if no other options are passed
$input->setOption('constants', true);
$input->setOption('properties', true);
$input->setOption('methods', true);
}
}
}

View file

@ -0,0 +1,129 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command\ListCommand;
use Psy\Reflection\ReflectionConstant;
use Symfony\Component\Console\Input\InputInterface;
/**
* Class Constant Enumerator class.
*/
class ClassConstantEnumerator extends Enumerator
{
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
{
// only list constants when a Reflector is present.
if ($reflector === null) {
return;
}
// We can only list constants on actual class (or object) reflectors.
if (!$reflector instanceof \ReflectionClass) {
// @todo handle ReflectionExtension as well
return;
}
// only list constants if we are specifically asked
if (!$input->getOption('constants')) {
return;
}
$noInherit = $input->getOption('no-inherit');
$constants = $this->prepareConstants($this->getConstants($reflector, $noInherit));
if (empty($constants)) {
return;
}
$ret = array();
$ret[$this->getKindLabel($reflector)] = $constants;
return $ret;
}
/**
* Get defined constants for the given class or object Reflector.
*
* @param \Reflector $reflector
* @param bool $noInherit Exclude inherited constants
*
* @return array
*/
protected function getConstants(\Reflector $reflector, $noInherit = false)
{
$className = $reflector->getName();
$constants = array();
foreach ($reflector->getConstants() as $name => $constant) {
$constReflector = new ReflectionConstant($reflector, $name);
if ($noInherit && $constReflector->getDeclaringClass()->getName() !== $className) {
continue;
}
$constants[$name] = $constReflector;
}
// @todo switch to ksort after we drop support for 5.3:
// ksort($constants, SORT_NATURAL | SORT_FLAG_CASE);
uksort($constants, 'strnatcasecmp');
return $constants;
}
/**
* Prepare formatted constant array.
*
* @param array $constants
*
* @return array
*/
protected function prepareConstants(array $constants)
{
// My kingdom for a generator.
$ret = array();
foreach ($constants as $name => $constant) {
if ($this->showItem($name)) {
$ret[$name] = array(
'name' => $name,
'style' => self::IS_CONSTANT,
'value' => $this->presentRef($constant->getValue()),
);
}
}
return $ret;
}
/**
* Get a label for the particular kind of "class" represented.
*
* @param \ReflectionClass $reflector
*
* @return string
*/
protected function getKindLabel(\ReflectionClass $reflector)
{
if ($reflector->isInterface()) {
return 'Interface Constants';
} elseif (method_exists($reflector, 'isTrait') && $reflector->isTrait()) {
return 'Trait Constants';
} else {
return 'Class Constants';
}
}
}

View file

@ -0,0 +1,126 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command\ListCommand;
use Symfony\Component\Console\Input\InputInterface;
/**
* Class Enumerator class.
*/
class ClassEnumerator extends Enumerator
{
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
{
// only list classes when no Reflector is present.
//
// @todo make a NamespaceReflector and pass that in for commands like:
//
// ls --classes Foo
//
// ... for listing classes in the Foo namespace
if ($reflector !== null || $target !== null) {
return;
}
$user = $input->getOption('user');
$internal = $input->getOption('internal');
$ret = array();
// only list classes, interfaces and traits if we are specifically asked
if ($input->getOption('classes')) {
$ret = array_merge($ret, $this->filterClasses('Classes', get_declared_classes(), $internal, $user));
}
if ($input->getOption('interfaces')) {
$ret = array_merge($ret, $this->filterClasses('Interfaces', get_declared_interfaces(), $internal, $user));
}
if (function_exists('get_declared_traits') && $input->getOption('traits')) {
$ret = array_merge($ret, $this->filterClasses('Traits', get_declared_traits(), $internal, $user));
}
return array_map(array($this, 'prepareClasses'), array_filter($ret));
}
/**
* Filter a list of classes, interfaces or traits.
*
* If $internal or $user is defined, results will be limited to internal or
* user-defined classes as appropriate.
*
* @param string $key
* @param array $classes
* @param bool $internal
* @param bool $user
*
* @return array
*/
protected function filterClasses($key, $classes, $internal, $user)
{
$ret = array();
if ($internal) {
$ret['Internal ' . $key] = array_filter($classes, function ($class) {
$refl = new \ReflectionClass($class);
return $refl->isInternal();
});
}
if ($user) {
$ret['User ' . $key] = array_filter($classes, function ($class) {
$refl = new \ReflectionClass($class);
return !$refl->isInternal();
});
}
if (!$user && !$internal) {
$ret[$key] = $classes;
}
return $ret;
}
/**
* Prepare formatted class array.
*
* @param array $class
*
* @return array
*/
protected function prepareClasses(array $classes)
{
natcasesort($classes);
// My kingdom for a generator.
$ret = array();
foreach ($classes as $name) {
if ($this->showItem($name)) {
$ret[$name] = array(
'name' => $name,
'style' => self::IS_CLASS,
'value' => $this->presentSignature($name),
);
}
}
return $ret;
}
}

View file

@ -0,0 +1,122 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command\ListCommand;
use Symfony\Component\Console\Input\InputInterface;
/**
* Constant Enumerator class.
*/
class ConstantEnumerator extends Enumerator
{
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
{
// only list constants when no Reflector is present.
//
// @todo make a NamespaceReflector and pass that in for commands like:
//
// ls --constants Foo
//
// ... for listing constants in the Foo namespace
if ($reflector !== null || $target !== null) {
return;
}
// only list constants if we are specifically asked
if (!$input->getOption('constants')) {
return;
}
$user = $input->getOption('user');
$internal = $input->getOption('internal');
$category = $input->getOption('category');
$ret = array();
if ($user) {
$ret['User Constants'] = $this->getConstants('user');
}
if ($internal) {
$ret['Interal Constants'] = $this->getConstants('internal');
}
if ($category) {
$label = ucfirst($category) . ' Constants';
$ret[$label] = $this->getConstants($category);
}
if (!$user && !$internal && !$category) {
$ret['Constants'] = $this->getConstants();
}
return array_map(array($this, 'prepareConstants'), array_filter($ret));
}
/**
* Get defined constants.
*
* Optionally restrict constants to a given category, e.g. "date". If the
* category is "internal", include all non-user-defined constants.
*
* @param string $category
*
* @return array
*/
protected function getConstants($category = null)
{
if (!$category) {
return get_defined_constants();
}
$consts = get_defined_constants(true);
if ($category === 'internal') {
unset($consts['user']);
return call_user_func_array('array_merge', $consts);
}
return isset($consts[$category]) ? $consts[$category] : array();
}
/**
* Prepare formatted constant array.
*
* @param array $constants
*
* @return array
*/
protected function prepareConstants(array $constants)
{
// My kingdom for a generator.
$ret = array();
$names = array_keys($constants);
natcasesort($names);
foreach ($names as $name) {
if ($this->showItem($name)) {
$ret[$name] = array(
'name' => $name,
'style' => self::IS_CONSTANT,
'value' => $this->presentRef($constants[$name]),
);
}
}
return $ret;
}
}

View file

@ -0,0 +1,106 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command\ListCommand;
use Psy\Formatter\SignatureFormatter;
use Psy\Input\FilterOptions;
use Psy\Util\Mirror;
use Psy\VarDumper\Presenter;
use Symfony\Component\Console\Input\InputInterface;
/**
* Abstract Enumerator class.
*/
abstract class Enumerator
{
// Output styles
const IS_PUBLIC = 'public';
const IS_PROTECTED = 'protected';
const IS_PRIVATE = 'private';
const IS_GLOBAL = 'global';
const IS_CONSTANT = 'const';
const IS_CLASS = 'class';
const IS_FUNCTION = 'function';
private $filter;
private $presenter;
/**
* Enumerator constructor.
*
* @param Presenter $presenter
*/
public function __construct(Presenter $presenter)
{
$this->filter = new FilterOptions();
$this->presenter = $presenter;
}
/**
* Return a list of categorized things with the given input options and target.
*
* @param InputInterface $input
* @param Reflector $reflector
* @param mixed $target
*
* @return array
*/
public function enumerate(InputInterface $input, \Reflector $reflector = null, $target = null)
{
$this->filter->bind($input);
return $this->listItems($input, $reflector, $target);
}
/**
* Enumerate specific items with the given input options and target.
*
* Implementing classes should return an array of arrays:
*
* [
* 'Constants' => [
* 'FOO' => [
* 'name' => 'FOO',
* 'style' => 'public',
* 'value' => '123',
* ],
* ],
* ]
*
* @param InputInterface $input
* @param Reflector $reflector
* @param mixed $target
*
* @return array
*/
abstract protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null);
protected function showItem($name)
{
return $this->filter->match($name);
}
protected function presentRef($value)
{
return $this->presenter->presentRef($value);
}
protected function presentSignature($target)
{
// This might get weird if the signature is actually for a reflector. Hrm.
if (!$target instanceof \Reflector) {
$target = Mirror::get($target);
}
return SignatureFormatter::format($target);
}
}

View file

@ -0,0 +1,112 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command\ListCommand;
use Symfony\Component\Console\Input\InputInterface;
/**
* Function Enumerator class.
*/
class FunctionEnumerator extends Enumerator
{
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
{
// only list functions when no Reflector is present.
//
// @todo make a NamespaceReflector and pass that in for commands like:
//
// ls --functions Foo
//
// ... for listing functions in the Foo namespace
if ($reflector !== null || $target !== null) {
return;
}
// only list functions if we are specifically asked
if (!$input->getOption('functions')) {
return;
}
if ($input->getOption('user')) {
$label = 'User Functions';
$functions = $this->getFunctions('user');
} elseif ($input->getOption('internal')) {
$label = 'Internal Functions';
$functions = $this->getFunctions('internal');
} else {
$label = 'Functions';
$functions = $this->getFunctions();
}
$functions = $this->prepareFunctions($functions);
if (empty($functions)) {
return;
}
$ret = array();
$ret[$label] = $functions;
return $ret;
}
/**
* Get defined functions.
*
* Optionally limit functions to "user" or "internal" functions.
*
* @param null|string $type "user" or "internal" (default: both)
*
* @return array
*/
protected function getFunctions($type = null)
{
$funcs = get_defined_functions();
if ($type) {
return $funcs[$type];
} else {
return array_merge($funcs['internal'], $funcs['user']);
}
}
/**
* Prepare formatted function array.
*
* @param array $functions
*
* @return array
*/
protected function prepareFunctions(array $functions)
{
natcasesort($functions);
// My kingdom for a generator.
$ret = array();
foreach ($functions as $name) {
if ($this->showItem($name)) {
$ret[$name] = array(
'name' => $name,
'style' => self::IS_FUNCTION,
'value' => $this->presentSignature($name),
);
}
}
return $ret;
}
}

View file

@ -0,0 +1,92 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command\ListCommand;
use Symfony\Component\Console\Input\InputInterface;
/**
* Global Variable Enumerator class.
*/
class GlobalVariableEnumerator extends Enumerator
{
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
{
// only list globals when no Reflector is present.
if ($reflector !== null || $target !== null) {
return;
}
// only list globals if we are specifically asked
if (!$input->getOption('globals')) {
return;
}
$globals = $this->prepareGlobals($this->getGlobals());
if (empty($globals)) {
return;
}
return array(
'Global Variables' => $globals,
);
}
/**
* Get defined global variables.
*
* @return array
*/
protected function getGlobals()
{
global $GLOBALS;
$names = array_keys($GLOBALS);
natcasesort($names);
$ret = array();
foreach ($names as $name) {
$ret[$name] = $GLOBALS[$name];
}
return $ret;
}
/**
* Prepare formatted global variable array.
*
* @param array $globals
*
* @return array
*/
protected function prepareGlobals($globals)
{
// My kingdom for a generator.
$ret = array();
foreach ($globals as $name => $value) {
if ($this->showItem($name)) {
$fname = '$' . $name;
$ret[$fname] = array(
'name' => $fname,
'style' => self::IS_GLOBAL,
'value' => $this->presentRef($value),
);
}
}
return $ret;
}
}

View file

@ -0,0 +1,88 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command\ListCommand;
use Psy\VarDumper\Presenter;
use Symfony\Component\Console\Input\InputInterface;
/**
* Interface Enumerator class.
*
* @deprecated Nothing should use this anymore
*/
class InterfaceEnumerator extends Enumerator
{
public function __construct(Presenter $presenter)
{
@trigger_error('InterfaceEnumerator is no longer used', E_USER_DEPRECATED);
}
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
{
// only list interfaces when no Reflector is present.
//
// @todo make a NamespaceReflector and pass that in for commands like:
//
// ls --interfaces Foo
//
// ... for listing interfaces in the Foo namespace
if ($reflector !== null || $target !== null) {
return;
}
// only list interfaces if we are specifically asked
if (!$input->getOption('interfaces')) {
return;
}
$interfaces = $this->prepareInterfaces(get_declared_interfaces());
if (empty($interfaces)) {
return;
}
return array(
'Interfaces' => $interfaces,
);
}
/**
* Prepare formatted interface array.
*
* @param array $interfaces
*
* @return array
*/
protected function prepareInterfaces(array $interfaces)
{
natcasesort($interfaces);
// My kingdom for a generator.
$ret = array();
foreach ($interfaces as $name) {
if ($this->showItem($name)) {
$ret[$name] = array(
'name' => $name,
'style' => self::IS_CLASS,
'value' => $this->presentSignature($name),
);
}
}
return $ret;
}
}

View file

@ -0,0 +1,147 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command\ListCommand;
use Symfony\Component\Console\Input\InputInterface;
/**
* Method Enumerator class.
*/
class MethodEnumerator extends Enumerator
{
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
{
// only list methods when a Reflector is present.
if ($reflector === null) {
return;
}
// We can only list methods on actual class (or object) reflectors.
if (!$reflector instanceof \ReflectionClass) {
return;
}
// only list methods if we are specifically asked
if (!$input->getOption('methods')) {
return;
}
$showAll = $input->getOption('all');
$noInherit = $input->getOption('no-inherit');
$methods = $this->prepareMethods($this->getMethods($showAll, $reflector, $noInherit));
if (empty($methods)) {
return;
}
$ret = array();
$ret[$this->getKindLabel($reflector)] = $methods;
return $ret;
}
/**
* Get defined methods for the given class or object Reflector.
*
* @param bool $showAll Include private and protected methods
* @param \Reflector $reflector
* @param bool $noInherit Exclude inherited methods
*
* @return array
*/
protected function getMethods($showAll, \Reflector $reflector, $noInherit = false)
{
$className = $reflector->getName();
$methods = array();
foreach ($reflector->getMethods() as $name => $method) {
if ($noInherit && $method->getDeclaringClass()->getName() !== $className) {
continue;
}
if ($showAll || $method->isPublic()) {
$methods[$method->getName()] = $method;
}
}
// @todo switch to ksort after we drop support for 5.3:
// ksort($methods, SORT_NATURAL | SORT_FLAG_CASE);
uksort($methods, 'strnatcasecmp');
return $methods;
}
/**
* Prepare formatted method array.
*
* @param array $methods
*
* @return array
*/
protected function prepareMethods(array $methods)
{
// My kingdom for a generator.
$ret = array();
foreach ($methods as $name => $method) {
if ($this->showItem($name)) {
$ret[$name] = array(
'name' => $name,
'style' => $this->getVisibilityStyle($method),
'value' => $this->presentSignature($method),
);
}
}
return $ret;
}
/**
* Get a label for the particular kind of "class" represented.
*
* @param \ReflectionClass $reflector
*
* @return string
*/
protected function getKindLabel(\ReflectionClass $reflector)
{
if ($reflector->isInterface()) {
return 'Interface Methods';
} elseif (method_exists($reflector, 'isTrait') && $reflector->isTrait()) {
return 'Trait Methods';
} else {
return 'Class Methods';
}
}
/**
* Get output style for the given method's visibility.
*
* @param \ReflectionMethod $method
*
* @return string
*/
private function getVisibilityStyle(\ReflectionMethod $method)
{
if ($method->isPublic()) {
return self::IS_PUBLIC;
} elseif ($method->isProtected()) {
return self::IS_PROTECTED;
} else {
return self::IS_PRIVATE;
}
}
}

View file

@ -0,0 +1,182 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command\ListCommand;
use Symfony\Component\Console\Input\InputInterface;
/**
* Property Enumerator class.
*/
class PropertyEnumerator extends Enumerator
{
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
{
// only list properties when a Reflector is present.
if ($reflector === null) {
return;
}
// We can only list properties on actual class (or object) reflectors.
if (!$reflector instanceof \ReflectionClass) {
return;
}
// only list properties if we are specifically asked
if (!$input->getOption('properties')) {
return;
}
$showAll = $input->getOption('all');
$noInherit = $input->getOption('no-inherit');
$properties = $this->prepareProperties($this->getProperties($showAll, $reflector, $noInherit), $target);
if (empty($properties)) {
return;
}
$ret = array();
$ret[$this->getKindLabel($reflector)] = $properties;
return $ret;
}
/**
* Get defined properties for the given class or object Reflector.
*
* @param bool $showAll Include private and protected properties
* @param \Reflector $reflector
* @param bool $noInherit Exclude inherited properties
*
* @return array
*/
protected function getProperties($showAll, \Reflector $reflector, $noInherit = false)
{
$className = $reflector->getName();
$properties = array();
foreach ($reflector->getProperties() as $property) {
if ($noInherit && $property->getDeclaringClass()->getName() !== $className) {
continue;
}
if ($showAll || $property->isPublic()) {
$properties[$property->getName()] = $property;
}
}
// @todo switch to ksort after we drop support for 5.3:
// ksort($properties, SORT_NATURAL | SORT_FLAG_CASE);
uksort($properties, 'strnatcasecmp');
return $properties;
}
/**
* Prepare formatted property array.
*
* @param array $properties
*
* @return array
*/
protected function prepareProperties(array $properties, $target = null)
{
// My kingdom for a generator.
$ret = array();
foreach ($properties as $name => $property) {
if ($this->showItem($name)) {
$fname = '$' . $name;
$ret[$fname] = array(
'name' => $fname,
'style' => $this->getVisibilityStyle($property),
'value' => $this->presentValue($property, $target),
);
}
}
return $ret;
}
/**
* Get a label for the particular kind of "class" represented.
*
* @param \ReflectionClass $reflector
*
* @return string
*/
protected function getKindLabel(\ReflectionClass $reflector)
{
if ($reflector->isInterface()) {
return 'Interface Properties';
} elseif (method_exists($reflector, 'isTrait') && $reflector->isTrait()) {
return 'Trait Properties';
} else {
return 'Class Properties';
}
}
/**
* Get output style for the given property's visibility.
*
* @param \ReflectionProperty $property
*
* @return string
*/
private function getVisibilityStyle(\ReflectionProperty $property)
{
if ($property->isPublic()) {
return self::IS_PUBLIC;
} elseif ($property->isProtected()) {
return self::IS_PROTECTED;
} else {
return self::IS_PRIVATE;
}
}
/**
* Present the $target's current value for a reflection property.
*
* @param \ReflectionProperty $property
* @param mixed $target
*
* @return string
*/
protected function presentValue(\ReflectionProperty $property, $target)
{
// If $target is a class, trait or interface (try to) get the default
// value for the property.
if (!is_object($target)) {
try {
$refl = new \ReflectionClass($target);
$props = $refl->getDefaultProperties();
if (array_key_exists($property->name, $props)) {
$suffix = $property->isStatic() ? '' : ' <aside>(default)</aside>';
return $this->presentRef($props[$property->name]) . $suffix;
}
} catch (\Exception $e) {
// Well, we gave it a shot.
}
return '';
}
$property->setAccessible(true);
$value = $property->getValue($target);
return $this->presentRef($value);
}
}

View file

@ -0,0 +1,93 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command\ListCommand;
use Psy\VarDumper\Presenter;
use Symfony\Component\Console\Input\InputInterface;
/**
* Trait Enumerator class.
*
* @deprecated Nothing should use this anymore
*/
class TraitEnumerator extends Enumerator
{
public function __construct(Presenter $presenter)
{
@trigger_error('TraitEnumerator is no longer used', E_USER_DEPRECATED);
}
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
{
// bail early if current PHP doesn't know about traits.
if (!function_exists('trait_exists')) {
return;
}
// only list traits when no Reflector is present.
//
// @todo make a NamespaceReflector and pass that in for commands like:
//
// ls --traits Foo
//
// ... for listing traits in the Foo namespace
if ($reflector !== null || $target !== null) {
return;
}
// only list traits if we are specifically asked
if (!$input->getOption('traits')) {
return;
}
$traits = $this->prepareTraits(get_declared_traits());
if (empty($traits)) {
return;
}
return array(
'Traits' => $traits,
);
}
/**
* Prepare formatted trait array.
*
* @param array $traits
*
* @return array
*/
protected function prepareTraits(array $traits)
{
natcasesort($traits);
// My kingdom for a generator.
$ret = array();
foreach ($traits as $name) {
if ($this->showItem($name)) {
$ret[$name] = array(
'name' => $name,
'style' => self::IS_CLASS,
'value' => $this->presentSignature($name),
);
}
}
return $ret;
}
}

View file

@ -0,0 +1,140 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command\ListCommand;
use Psy\Context;
use Psy\VarDumper\Presenter;
use Symfony\Component\Console\Input\InputInterface;
/**
* Variable Enumerator class.
*/
class VariableEnumerator extends Enumerator
{
// n.b. this array is the order in which special variables will be listed
private static $specialNames = array(
'_', '_e', '__out', '__function', '__method', '__class', '__namespace', '__file', '__line', '__dir',
);
private $context;
/**
* Variable Enumerator constructor.
*
* Unlike most other enumerators, the Variable Enumerator needs access to
* the current scope variables, so we need to pass it a Context instance.
*
* @param Presenter $presenter
* @param Context $context
*/
public function __construct(Presenter $presenter, Context $context)
{
$this->context = $context;
parent::__construct($presenter);
}
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
{
// only list variables when no Reflector is present.
if ($reflector !== null || $target !== null) {
return;
}
// only list variables if we are specifically asked
if (!$input->getOption('vars')) {
return;
}
$showAll = $input->getOption('all');
$variables = $this->prepareVariables($this->getVariables($showAll));
if (empty($variables)) {
return;
}
return array(
'Variables' => $variables,
);
}
/**
* Get scope variables.
*
* @param bool $showAll Include special variables (e.g. $_)
*
* @return array
*/
protected function getVariables($showAll)
{
// self:: doesn't work inside closures in PHP 5.3 :-/
$specialNames = self::$specialNames;
$scopeVars = $this->context->getAll();
uksort($scopeVars, function ($a, $b) use ($specialNames) {
$aIndex = array_search($a, $specialNames);
$bIndex = array_search($b, $specialNames);
if ($aIndex !== false) {
if ($bIndex !== false) {
return $aIndex - $bIndex;
}
return 1;
}
if ($bIndex !== false) {
return -1;
}
return strnatcasecmp($a, $b);
});
$ret = array();
foreach ($scopeVars as $name => $val) {
if (!$showAll && in_array($name, self::$specialNames)) {
continue;
}
$ret[$name] = $val;
}
return $ret;
}
/**
* Prepare formatted variable array.
*
* @param array $variables
*
* @return array
*/
protected function prepareVariables(array $variables)
{
// My kingdom for a generator.
$ret = array();
foreach ($variables as $name => $val) {
if ($this->showItem($name)) {
$fname = '$' . $name;
$ret[$fname] = array(
'name' => $fname,
'style' => in_array($name, self::$specialNames) ? self::IS_PRIVATE : self::IS_PUBLIC,
'value' => $this->presentRef($val),
);
}
}
return $ret;
}
}

View file

@ -0,0 +1,160 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use PhpParser\Node;
use PhpParser\Parser;
use Psy\Input\CodeArgument;
use Psy\ParserFactory;
use Psy\VarDumper\Presenter;
use Psy\VarDumper\PresenterAware;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\VarDumper\Caster\Caster;
/**
* Parse PHP code and show the abstract syntax tree.
*/
class ParseCommand extends Command implements PresenterAware
{
private $presenter;
private $parserFactory;
private $parsers;
/**
* {@inheritdoc}
*/
public function __construct($name = null)
{
$this->parserFactory = new ParserFactory();
$this->parsers = array();
parent::__construct($name);
}
/**
* PresenterAware interface.
*
* @param Presenter $presenter
*/
public function setPresenter(Presenter $presenter)
{
$this->presenter = clone $presenter;
$this->presenter->addCasters(array(
'PhpParser\Node' => function (Node $node, array $a) {
$a = array(
Caster::PREFIX_VIRTUAL . 'type' => $node->getType(),
Caster::PREFIX_VIRTUAL . 'attributes' => $node->getAttributes(),
);
foreach ($node->getSubNodeNames() as $name) {
$a[Caster::PREFIX_VIRTUAL . $name] = $node->$name;
}
return $a;
},
));
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$definition = array(
new CodeArgument('code', InputArgument::REQUIRED, 'PHP code to parse.'),
new InputOption('depth', '', InputOption::VALUE_REQUIRED, 'Depth to parse', 10),
);
if ($this->parserFactory->hasKindsSupport()) {
$msg = 'One of PhpParser\\ParserFactory constants: '
. implode(', ', ParserFactory::getPossibleKinds())
. " (default is based on current interpreter's version)";
$defaultKind = $this->parserFactory->getDefaultKind();
$definition[] = new InputOption('kind', '', InputOption::VALUE_REQUIRED, $msg, $defaultKind);
}
$this
->setName('parse')
->setDefinition($definition)
->setDescription('Parse PHP code and show the abstract syntax tree.')
->setHelp(
<<<'HELP'
Parse PHP code and show the abstract syntax tree.
This command is used in the development of PsySH. Given a string of PHP code,
it pretty-prints the PHP Parser parse tree.
See https://github.com/nikic/PHP-Parser
It prolly won't be super useful for most of you, but it's here if you want to play.
HELP
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$code = $input->getArgument('code');
if (strpos('<?', $code) === false) {
$code = '<?php ' . $code;
}
$parserKind = $this->parserFactory->hasKindsSupport() ? $input->getOption('kind') : null;
$depth = $input->getOption('depth');
$nodes = $this->parse($this->getParser($parserKind), $code);
$output->page($this->presenter->present($nodes, $depth));
}
/**
* Lex and parse a string of code into statements.
*
* @param Parser $parser
* @param string $code
*
* @return array Statements
*/
private function parse(Parser $parser, $code)
{
try {
return $parser->parse($code);
} catch (\PhpParser\Error $e) {
if (strpos($e->getMessage(), 'unexpected EOF') === false) {
throw $e;
}
// If we got an unexpected EOF, let's try it again with a semicolon.
return $parser->parse($code . ';');
}
}
/**
* Get (or create) the Parser instance.
*
* @param string|null $kind One of Psy\ParserFactory constants (only for PHP parser 2.0 and above)
*
* @return Parser
*/
private function getParser($kind = null)
{
if (!array_key_exists($kind, $this->parsers)) {
$this->parsers[$kind] = $this->parserFactory->createParser($kind);
}
return $this->parsers[$kind];
}
}

View file

@ -0,0 +1,41 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* A dumb little command for printing out the current Psy Shell version.
*/
class PsyVersionCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('version')
->setDefinition(array())
->setDescription('Show Psy Shell version.')
->setHelp('Show Psy Shell version.');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln($this->getApplication()->getVersion());
}
}

View file

@ -0,0 +1,255 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Psy\Context;
use Psy\ContextAware;
use Psy\Exception\RuntimeException;
use Psy\Util\Mirror;
/**
* An abstract command with helpers for inspecting the current context.
*/
abstract class ReflectingCommand extends Command implements ContextAware
{
const CLASS_OR_FUNC = '/^[\\\\\w]+$/';
const INSTANCE = '/^\$(\w+)$/';
const CLASS_MEMBER = '/^([\\\\\w]+)::(\w+)$/';
const CLASS_STATIC = '/^([\\\\\w]+)::\$(\w+)$/';
const INSTANCE_MEMBER = '/^\$(\w+)(::|->)(\w+)$/';
const INSTANCE_STATIC = '/^\$(\w+)::\$(\w+)$/';
const SUPERGLOBAL = '/^\$(GLOBALS|_(?:SERVER|ENV|FILES|COOKIE|POST|GET|SESSION))$/';
/**
* Context instance (for ContextAware interface).
*
* @var Context
*/
protected $context;
/**
* ContextAware interface.
*
* @param Context $context
*/
public function setContext(Context $context)
{
$this->context = $context;
}
/**
* Get the target for a value.
*
* @throws \InvalidArgumentException when the value specified can't be resolved
*
* @param string $valueName Function, class, variable, constant, method or property name
* @param bool $classOnly True if the name should only refer to a class, function or instance
*
* @return array (class or instance name, member name, kind)
*/
protected function getTarget($valueName, $classOnly = false)
{
$valueName = trim($valueName);
$matches = array();
switch (true) {
case preg_match(self::SUPERGLOBAL, $valueName, $matches):
// @todo maybe do something interesting with these at some point?
if (array_key_exists($matches[1], $GLOBALS)) {
throw new RuntimeException('Unable to inspect a non-object');
}
throw new RuntimeException('Unknown target: ' . $valueName);
case preg_match(self::CLASS_OR_FUNC, $valueName, $matches):
return array($this->resolveName($matches[0], true), null, 0);
case preg_match(self::INSTANCE, $valueName, $matches):
return array($this->resolveInstance($matches[1]), null, 0);
case !$classOnly && preg_match(self::CLASS_MEMBER, $valueName, $matches):
return array($this->resolveName($matches[1]), $matches[2], Mirror::CONSTANT | Mirror::METHOD);
case !$classOnly && preg_match(self::CLASS_STATIC, $valueName, $matches):
return array($this->resolveName($matches[1]), $matches[2], Mirror::STATIC_PROPERTY | Mirror::PROPERTY);
case !$classOnly && preg_match(self::INSTANCE_MEMBER, $valueName, $matches):
if ($matches[2] === '->') {
$kind = Mirror::METHOD | Mirror::PROPERTY;
} else {
$kind = Mirror::CONSTANT | Mirror::METHOD;
}
return array($this->resolveInstance($matches[1]), $matches[3], $kind);
case !$classOnly && preg_match(self::INSTANCE_STATIC, $valueName, $matches):
return array($this->resolveInstance($matches[1]), $matches[2], Mirror::STATIC_PROPERTY);
default:
throw new RuntimeException('Unknown target: ' . $valueName);
}
}
/**
* Resolve a class or function name (with the current shell namespace).
*
* @param string $name
* @param bool $includeFunctions (default: false)
*
* @return string
*/
protected function resolveName($name, $includeFunctions = false)
{
if (substr($name, 0, 1) === '\\') {
return $name;
}
if ($namespace = $this->getApplication()->getNamespace()) {
$fullName = $namespace . '\\' . $name;
if (class_exists($fullName) || interface_exists($fullName) || ($includeFunctions && function_exists($fullName))) {
return $fullName;
}
}
return $name;
}
/**
* Get a Reflector and documentation for a function, class or instance, constant, method or property.
*
* @param string $valueName Function, class, variable, constant, method or property name
* @param bool $classOnly True if the name should only refer to a class, function or instance
*
* @return array (value, Reflector)
*/
protected function getTargetAndReflector($valueName, $classOnly = false)
{
list($value, $member, $kind) = $this->getTarget($valueName, $classOnly);
return array($value, Mirror::get($value, $member, $kind));
}
/**
* Return a variable instance from the current scope.
*
* @throws \InvalidArgumentException when the requested variable does not exist in the current scope
*
* @param string $name
*
* @return mixed Variable instance
*/
protected function resolveInstance($name)
{
$value = $this->getScopeVariable($name);
if (!is_object($value)) {
throw new RuntimeException('Unable to inspect a non-object');
}
return $value;
}
/**
* Get a variable from the current shell scope.
*
* @param string $name
*
* @return mixed
*/
protected function getScopeVariable($name)
{
return $this->context->get($name);
}
/**
* Get all scope variables from the current shell scope.
*
* @return array
*/
protected function getScopeVariables()
{
return $this->context->getAll();
}
/**
* Given a Reflector instance, set command-scope variables in the shell
* execution context. This is used to inject magic $__class, $__method and
* $__file variables (as well as a handful of others).
*
* @param \Reflector $reflector
*/
protected function setCommandScopeVariables(\Reflector $reflector)
{
$vars = array();
switch (get_class($reflector)) {
case 'ReflectionClass':
case 'ReflectionObject':
$vars['__class'] = $reflector->name;
if ($reflector->inNamespace()) {
$vars['__namespace'] = $reflector->getNamespaceName();
}
break;
case 'ReflectionMethod':
$vars['__method'] = sprintf('%s::%s', $reflector->class, $reflector->name);
$vars['__class'] = $reflector->class;
$classReflector = $reflector->getDeclaringClass();
if ($classReflector->inNamespace()) {
$vars['__namespace'] = $classReflector->getNamespaceName();
}
break;
case 'ReflectionFunction':
$vars['__function'] = $reflector->name;
if ($reflector->inNamespace()) {
$vars['__namespace'] = $reflector->getNamespaceName();
}
break;
case 'ReflectionGenerator':
$funcReflector = $reflector->getFunction();
$vars['__function'] = $funcReflector->name;
if ($funcReflector->inNamespace()) {
$vars['__namespace'] = $funcReflector->getNamespaceName();
}
if ($fileName = $reflector->getExecutingFile()) {
$vars['__file'] = $fileName;
$vars['__line'] = $reflector->getExecutingLine();
$vars['__dir'] = dirname($fileName);
}
break;
case 'ReflectionProperty':
case 'Psy\Reflection\ReflectionConstant':
$classReflector = $reflector->getDeclaringClass();
$vars['__class'] = $classReflector->name;
if ($classReflector->inNamespace()) {
$vars['__namespace'] = $classReflector->getNamespaceName();
}
// no line for these, but this'll do
if ($fileName = $reflector->getDeclaringClass()->getFileName()) {
$vars['__file'] = $fileName;
$vars['__dir'] = dirname($fileName);
}
break;
}
if ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionFunctionAbstract) {
if ($fileName = $reflector->getFileName()) {
$vars['__file'] = $fileName;
$vars['__line'] = $reflector->getStartLine();
$vars['__dir'] = dirname($fileName);
}
}
$this->context->setCommandScopeVariables($vars);
}
}

View file

@ -0,0 +1,272 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use JakubOnderka\PhpConsoleHighlighter\Highlighter;
use Psy\Configuration;
use Psy\ConsoleColorFactory;
use Psy\Exception\RuntimeException;
use Psy\Formatter\CodeFormatter;
use Psy\Formatter\SignatureFormatter;
use Psy\Output\ShellOutput;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Show the code for an object, class, constant, method or property.
*/
class ShowCommand extends ReflectingCommand
{
private $colorMode;
private $highlighter;
private $lastException;
private $lastExceptionIndex;
/**
* @param null|string $colorMode (default: null)
*/
public function __construct($colorMode = null)
{
$this->colorMode = $colorMode ?: Configuration::COLOR_MODE_AUTO;
return parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('show')
->setDefinition(array(
new InputArgument('value', InputArgument::OPTIONAL, 'Function, class, instance, constant, method or property to show.'),
new InputOption('ex', null, InputOption::VALUE_OPTIONAL, 'Show last exception context. Optionally specify a stack index.', 1),
))
->setDescription('Show the code for an object, class, constant, method or property.')
->setHelp(
<<<HELP
Show the code for an object, class, constant, method or property, or the context
of the last exception.
<return>cat --ex</return> defaults to showing the lines surrounding the location of the last
exception. Invoking it more than once travels up the exception's stack trace,
and providing a number shows the context of the given index of the trace.
e.g.
<return>>>> show \$myObject</return>
<return>>>> show Psy\Shell::debug</return>
<return>>>> show --ex</return>
<return>>>> show --ex 3</return>
HELP
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
// n.b. As far as I can tell, InputInterface doesn't want to tell me
// whether an option with an optional value was actually passed. If you
// call `$input->getOption('ex')`, it will return the default, both when
// `--ex` is specified with no value, and when `--ex` isn't specified at
// all.
//
// So we're doing something sneaky here. If we call `getOptions`, it'll
// return the default value when `--ex` is not present, and `null` if
// `--ex` is passed with no value. /shrug
$opts = $input->getOptions();
// Strict comparison to `1` (the default value) here, because `--ex 1`
// will come in as `"1"`. Now we can tell the difference between
// "no --ex present", because it's the integer 1, "--ex with no value",
// because it's `null`, and "--ex 1", because it's the string "1".
if ($opts['ex'] !== 1) {
if ($input->getArgument('value')) {
throw new \InvalidArgumentException('Too many arguments (supply either "value" or "--ex")');
}
return $this->writeExceptionContext($input, $output);
}
if ($input->getArgument('value')) {
return $this->writeCodeContext($input, $output);
}
throw new RuntimeException('Not enough arguments (missing: "value").');
}
private function writeCodeContext(InputInterface $input, OutputInterface $output)
{
list($value, $reflector) = $this->getTargetAndReflector($input->getArgument('value'));
// Set some magic local variables
$this->setCommandScopeVariables($reflector);
try {
$output->page(CodeFormatter::format($reflector, $this->colorMode), ShellOutput::OUTPUT_RAW);
} catch (RuntimeException $e) {
$output->writeln(SignatureFormatter::format($reflector));
throw $e;
}
}
private function writeExceptionContext(InputInterface $input, OutputInterface $output)
{
$exception = $this->context->getLastException();
if ($exception !== $this->lastException) {
$this->lastException = null;
$this->lastExceptionIndex = null;
}
$opts = $input->getOptions();
if ($opts['ex'] === null) {
if ($this->lastException && $this->lastExceptionIndex !== null) {
$index = $this->lastExceptionIndex + 1;
} else {
$index = 0;
}
} else {
$index = max(0, intval($input->getOption('ex')) - 1);
}
$trace = $exception->getTrace();
array_unshift($trace, array(
'file' => $exception->getFile(),
'line' => $exception->getLine(),
));
if ($index >= count($trace)) {
$index = 0;
}
$this->lastException = $exception;
$this->lastExceptionIndex = $index;
$output->writeln($this->getApplication()->formatException($exception));
$output->writeln('--');
$this->writeTraceLine($output, $trace, $index);
$this->writeTraceCodeSnippet($output, $trace, $index);
$this->setCommandScopeVariablesFromContext($trace[$index]);
}
private function writeTraceLine(OutputInterface $output, array $trace, $index)
{
$file = isset($trace[$index]['file']) ? $this->replaceCwd($trace[$index]['file']) : 'n/a';
$line = isset($trace[$index]['line']) ? $trace[$index]['line'] : 'n/a';
$output->writeln(sprintf(
'From <info>%s:%d</info> at <strong>level %d</strong> of backtrace (of %d).',
OutputFormatter::escape($file),
OutputFormatter::escape($line),
$index + 1,
count($trace)
));
}
private function replaceCwd($file)
{
if ($cwd = getcwd()) {
$cwd = rtrim($cwd, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
}
if ($cwd === false) {
return $file;
} else {
return preg_replace('/^' . preg_quote($cwd, '/') . '/', '', $file);
}
}
private function writeTraceCodeSnippet(OutputInterface $output, array $trace, $index)
{
if (!isset($trace[$index]['file'])) {
return;
}
$file = $trace[$index]['file'];
if ($fileAndLine = $this->extractEvalFileAndLine($file)) {
list($file, $line) = $fileAndLine;
} else {
if (!isset($trace[$index]['line'])) {
return;
}
$line = $trace[$index]['line'];
}
if (is_file($file)) {
$code = @file_get_contents($file);
}
if (empty($code)) {
return;
}
$output->write($this->getHighlighter()->getCodeSnippet($code, $line, 5, 5), ShellOutput::OUTPUT_RAW);
}
private function getHighlighter()
{
if (!$this->highlighter) {
$factory = new ConsoleColorFactory($this->colorMode);
$this->highlighter = new Highlighter($factory->getConsoleColor());
}
return $this->highlighter;
}
private function setCommandScopeVariablesFromContext(array $context)
{
$vars = array();
// @todo __namespace?
if (isset($context['class'])) {
$vars['__class'] = $context['class'];
if (isset($context['function'])) {
$vars['__method'] = $context['function'];
}
} elseif (isset($context['function'])) {
$vars['__function'] = $context['function'];
}
if (isset($context['file'])) {
$file = $context['file'];
if ($fileAndLine = $this->extractEvalFileAndLine($file)) {
list($file, $line) = $fileAndLine;
} elseif (isset($context['line'])) {
$line = $context['line'];
}
if (is_file($file)) {
$vars['__file'] = $file;
if (isset($line)) {
$vars['__line'] = $line;
}
$vars['__dir'] = dirname($file);
}
}
$this->context->setCommandScopeVariables($vars);
}
private function extractEvalFileAndLine($file)
{
if (preg_match('/(.*)\\((\\d+)\\) : eval\\(\\)\'d code$/', $file, $matches)) {
return array($matches[1], $matches[2]);
}
}
}

View file

@ -0,0 +1,143 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use PhpParser\NodeTraverser;
use PhpParser\PrettyPrinter\Standard as Printer;
use Psy\Input\CodeArgument;
use Psy\ParserFactory;
use Psy\Readline\Readline;
use Psy\Sudo\SudoVisitor;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Evaluate PHP code, bypassing visibility restrictions.
*/
class SudoCommand extends Command
{
private $readline;
private $parser;
private $traverser;
private $printer;
/**
* {@inheritdoc}
*/
public function __construct($name = null)
{
$parserFactory = new ParserFactory();
$this->parser = $parserFactory->createParser();
$this->traverser = new NodeTraverser();
$this->traverser->addVisitor(new SudoVisitor());
$this->printer = new Printer();
parent::__construct($name);
}
/**
* Set the Shell's Readline service.
*
* @param Readline $readline
*/
public function setReadline(Readline $readline)
{
$this->readline = $readline;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('sudo')
->setDefinition(array(
new CodeArgument('code', InputArgument::REQUIRED, 'Code to execute.'),
))
->setDescription('Evaluate PHP code, bypassing visibility restrictions.')
->setHelp(
<<<'HELP'
Evaluate PHP code, bypassing visibility restrictions.
e.g.
<return>>>> $sekret->whisper("hi")</return>
<return>PHP error: Call to private method Sekret::whisper() from context '' on line 1</return>
<return>>>> sudo $sekret->whisper("hi")</return>
<return>=> "hi"</return>
<return>>>> $sekret->word</return>
<return>PHP error: Cannot access private property Sekret::$word on line 1</return>
<return>>>> sudo $sekret->word</return>
<return>=> "hi"</return>
<return>>>> $sekret->word = "please"</return>
<return>PHP error: Cannot access private property Sekret::$word on line 1</return>
<return>>>> sudo $sekret->word = "please"</return>
<return>=> "please"</return>
HELP
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$code = $input->getArgument('code');
// special case for !!
if ($code === '!!') {
$history = $this->readline->listHistory();
if (count($history) < 2) {
throw new \InvalidArgumentException('No previous command to replay');
}
$code = $history[count($history) - 2];
}
if (strpos('<?', $code) === false) {
$code = '<?php ' . $code;
}
$nodes = $this->traverser->traverse($this->parse($code));
$sudoCode = $this->printer->prettyPrint($nodes);
$this->getApplication()->addInput($sudoCode, true);
}
/**
* Lex and parse a string of code into statements.
*
* @param string $code
*
* @return array Statements
*/
private function parse($code)
{
try {
return $this->parser->parse($code);
} catch (\PhpParser\Error $e) {
if (strpos($e->getMessage(), 'unexpected EOF') === false) {
throw $e;
}
// If we got an unexpected EOF, let's try it again with a semicolon.
return $this->parser->parse($code . ';');
}
}
}

View file

@ -0,0 +1,87 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Psy\Context;
use Psy\ContextAware;
use Psy\Exception\ThrowUpException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Throw an exception out of the Psy Shell.
*/
class ThrowUpCommand extends Command implements ContextAware
{
/**
* Context instance (for ContextAware interface).
*
* @var Context
*/
protected $context;
/**
* ContextAware interface.
*
* @param Context $context
*/
public function setContext(Context $context)
{
$this->context = $context;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('throw-up')
->setDefinition(array(
new InputArgument('exception', InputArgument::OPTIONAL, 'Exception to throw'),
))
->setDescription('Throw an exception out of the Psy Shell.')
->setHelp(
<<<'HELP'
Throws an exception out of the current the Psy Shell instance.
By default it throws the most recent exception.
e.g.
<return>>>> throw-up</return>
<return>>>> throw-up $e</return>
HELP
);
}
/**
* {@inheritdoc}
*
* @throws InvalidArgumentException if there is no exception to throw
* @throws ThrowUpException because what else do you expect it to do?
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
if ($name = $input->getArgument('exception')) {
$orig = $this->context->get(preg_replace('/^\$/', '', $name));
} else {
$orig = $this->context->getLastException();
}
if (!$orig instanceof \Exception) {
throw new \InvalidArgumentException('throw-up can only throw Exceptions');
}
throw new ThrowUpException($orig);
}
}

View file

@ -0,0 +1,167 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Psy\Input\FilterOptions;
use Psy\Output\ShellOutput;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Show the current stack trace.
*/
class TraceCommand extends Command
{
protected $filter;
/**
* {@inheritdoc}
*/
public function __construct($name = null)
{
$this->filter = new FilterOptions();
parent::__construct($name);
}
/**
* {@inheritdoc}
*/
protected function configure()
{
list($grep, $insensitive, $invert) = FilterOptions::getOptions();
$this
->setName('trace')
->setDefinition(array(
new InputOption('include-psy', 'p', InputOption::VALUE_NONE, 'Include Psy in the call stack.'),
new InputOption('num', 'n', InputOption::VALUE_REQUIRED, 'Only include NUM lines.'),
$grep,
$insensitive,
$invert,
))
->setDescription('Show the current call stack.')
->setHelp(
<<<'HELP'
Show the current call stack.
Optionally, include PsySH in the call stack by passing the <info>--include-psy</info> option.
e.g.
<return>> trace -n10</return>
<return>> trace --include-psy</return>
HELP
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->filter->bind($input);
$trace = $this->getBacktrace(new \Exception(), $input->getOption('num'), $input->getOption('include-psy'));
$output->page($trace, ShellOutput::NUMBER_LINES);
}
/**
* Get a backtrace for an exception.
*
* Optionally limit the number of rows to include with $count, and exclude
* Psy from the trace.
*
* @param \Exception $e The exception with a backtrace
* @param int $count (default: PHP_INT_MAX)
* @param bool $includePsy (default: true)
*
* @return array Formatted stacktrace lines
*/
protected function getBacktrace(\Exception $e, $count = null, $includePsy = true)
{
if ($cwd = getcwd()) {
$cwd = rtrim($cwd, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
}
if ($count === null) {
$count = PHP_INT_MAX;
}
$lines = array();
$trace = $e->getTrace();
array_unshift($trace, array(
'function' => '',
'file' => $e->getFile() !== null ? $e->getFile() : 'n/a',
'line' => $e->getLine() !== null ? $e->getLine() : 'n/a',
'args' => array(),
));
if (!$includePsy) {
for ($i = count($trace) - 1; $i >= 0; $i--) {
$thing = isset($trace[$i]['class']) ? $trace[$i]['class'] : $trace[$i]['function'];
if (preg_match('/\\\\?Psy\\\\/', $thing)) {
$trace = array_slice($trace, $i + 1);
break;
}
}
}
for ($i = 0, $count = min($count, count($trace)); $i < $count; $i++) {
$class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
$type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
$function = $trace[$i]['function'];
$file = isset($trace[$i]['file']) ? $this->replaceCwd($cwd, $trace[$i]['file']) : 'n/a';
$line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
// Leave execution loop out of the `eval()'d code` lines
if (preg_match("#/Psy/ExecutionLoop/Loop.php\(\d+\) : eval\(\)'d code$#", $file)) {
$file = "eval()'d code";
}
// Skip any lines that don't match our filter options
if (!$this->filter->match(sprintf('%s%s%s() at %s:%s', $class, $type, $function, $file, $line))) {
continue;
}
$lines[] = sprintf(
' <class>%s</class>%s%s() at <info>%s:%s</info>',
OutputFormatter::escape($class),
OutputFormatter::escape($type),
OutputFormatter::escape($function),
OutputFormatter::escape($file),
OutputFormatter::escape($line)
);
}
return $lines;
}
/**
* Replace the given directory from the start of a filepath.
*
* @param string $cwd
* @param string $file
*
* @return string
*/
private function replaceCwd($cwd, $file)
{
if ($cwd === false) {
return $file;
} else {
return preg_replace('/^' . preg_quote($cwd, '/') . '/', '', $file);
}
}
}

View file

@ -0,0 +1,152 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use JakubOnderka\PhpConsoleHighlighter\Highlighter;
use Psy\Configuration;
use Psy\ConsoleColorFactory;
use Psy\Output\ShellOutput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Show the context of where you opened the debugger.
*/
class WhereamiCommand extends Command
{
private $colorMode;
/**
* @param null|string $colorMode (default: null)
*/
public function __construct($colorMode = null)
{
$this->colorMode = $colorMode ?: Configuration::COLOR_MODE_AUTO;
if (version_compare(PHP_VERSION, '5.3.6', '>=')) {
$this->backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
} else {
$this->backtrace = debug_backtrace();
}
return parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('whereami')
->setDefinition(array(
new InputOption('num', 'n', InputOption::VALUE_OPTIONAL, 'Number of lines before and after.', '5'),
))
->setDescription('Show where you are in the code.')
->setHelp(
<<<'HELP'
Show where you are in the code.
Optionally, include how many lines before and after you want to display.
e.g.
<return>> whereami </return>
<return>> whereami -n10</return>
HELP
);
}
/**
* Obtains the correct stack frame in the full backtrace.
*
* @return array
*/
protected function trace()
{
foreach (array_reverse($this->backtrace) as $stackFrame) {
if ($this->isDebugCall($stackFrame)) {
return $stackFrame;
}
}
return end($this->backtrace);
}
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' && in_array($function, array('__construct', 'debug')));
}
/**
* Determine the file and line based on the specific backtrace.
*
* @return array
*/
protected function fileInfo()
{
$stackFrame = $this->trace();
if (preg_match('/eval\(/', $stackFrame['file'])) {
preg_match_all('/([^\(]+)\((\d+)/', $stackFrame['file'], $matches);
$file = $matches[1][0];
$line = (int) $matches[2][0];
} else {
$file = $stackFrame['file'];
$line = $stackFrame['line'];
}
return compact('file', 'line');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$info = $this->fileInfo();
$num = $input->getOption('num');
$factory = new ConsoleColorFactory($this->colorMode);
$colors = $factory->getConsoleColor();
$highlighter = new Highlighter($colors);
$contents = file_get_contents($info['file']);
$output->startPaging();
$output->writeln('');
$output->writeln(sprintf('From <info>%s:%s</info>:', $this->replaceCwd($info['file']), $info['line']));
$output->writeln('');
$output->write($highlighter->getCodeSnippet($contents, $info['line'], $num, $num), ShellOutput::OUTPUT_RAW);
$output->stopPaging();
}
/**
* Replace the given directory from the start of a filepath.
*
* @param string $file
*
* @return string
*/
private function replaceCwd($file)
{
$cwd = getcwd();
if ($cwd === false) {
return $file;
}
$cwd = rtrim($cwd, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
return preg_replace('/^' . preg_quote($cwd, '/') . '/', '', $file);
}
}

View file

@ -0,0 +1,125 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command;
use Psy\Context;
use Psy\ContextAware;
use Psy\Input\FilterOptions;
use Psy\Output\ShellOutput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Show the last uncaught exception.
*/
class WtfCommand extends TraceCommand implements ContextAware
{
/**
* Context instance (for ContextAware interface).
*
* @var Context
*/
protected $context;
/**
* ContextAware interface.
*
* @param Context $context
*/
public function setContext(Context $context)
{
$this->context = $context;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
list($grep, $insensitive, $invert) = FilterOptions::getOptions();
$this
->setName('wtf')
->setAliases(array('last-exception', 'wtf?'))
->setDefinition(array(
new InputArgument('incredulity', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Number of lines to show'),
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show entire backtrace.'),
$grep,
$insensitive,
$invert,
))
->setDescription('Show the backtrace of the most recent exception.')
->setHelp(
<<<'HELP'
Shows a few lines of the backtrace of the most recent exception.
If you want to see more lines, add more question marks or exclamation marks:
e.g.
<return>>>> wtf ?</return>
<return>>>> wtf ?!???!?!?</return>
To see the entire backtrace, pass the -a/--all flag:
e.g.
<return>>>> wtf -v</return>
HELP
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->filter->bind($input);
$incredulity = implode('', $input->getArgument('incredulity'));
if (strlen(preg_replace('/[\\?!]/', '', $incredulity))) {
throw new \InvalidArgumentException('Incredulity must include only "?" and "!".');
}
$exception = $this->context->getLastException();
$count = $input->getOption('all') ? PHP_INT_MAX : max(3, pow(2, strlen($incredulity) + 1));
$shell = $this->getApplication();
$output->startPaging();
do {
$traceCount = count($exception->getTrace());
$showLines = $count;
// Show the whole trace if we'd only be hiding a few lines
if ($traceCount < max($count * 1.2, $count + 2)) {
$showLines = PHP_INT_MAX;
}
$trace = $this->getBacktrace($exception, $showLines);
$moreLines = $traceCount - count($trace);
$output->writeln($shell->formatException($exception));
$output->writeln('--');
$output->write($trace, true, ShellOutput::NUMBER_LINES);
$output->writeln('');
if ($moreLines > 0) {
$output->writeln(sprintf(
'<aside>Use <return>wtf -a</return> to see %d more lines</aside>',
$moreLines
));
$output->writeln('');
}
} while ($exception = $exception->getPrevious());
$output->stopPaging();
}
}

163
vendor/psy/psysh/src/Psy/Compiler.php vendored Normal file
View file

@ -0,0 +1,163 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy;
use Symfony\Component\Finder\Finder;
/**
* A Psy Shell Phar compiler.
*/
class Compiler
{
/**
* Compiles psysh into a single phar file.
*
* @param string $pharFile The full path to the file to create
*/
public function compile($pharFile = 'psysh.phar')
{
if (file_exists($pharFile)) {
unlink($pharFile);
}
$this->version = Shell::VERSION;
$phar = new \Phar($pharFile, 0, 'psysh.phar');
$phar->setSignatureAlgorithm(\Phar::SHA1);
$phar->startBuffering();
$finder = Finder::create()
->files()
->ignoreVCS(true)
->name('*.php')
->notName('Compiler.php')
->notName('Autoloader.php')
->in(__DIR__ . '/..');
foreach ($finder as $file) {
$this->addFile($phar, $file);
}
$finder = Finder::create()
->files()
->ignoreVCS(true)
->name('*.php')
->exclude('Tests')
->exclude('tests')
->exclude('Test')
->exclude('test')
->in(__DIR__ . '/../../build-vendor');
foreach ($finder as $file) {
$this->addFile($phar, $file);
}
// Stubs
$phar->setStub($this->getStub());
$phar->stopBuffering();
unset($phar);
}
/**
* Add a file to the psysh Phar.
*
* @param \Phar $phar
* @param \SplFileInfo $file
* @param bool $strip (default: true)
*/
private function addFile($phar, $file, $strip = true)
{
$path = str_replace(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR, '', $file->getRealPath());
$content = file_get_contents($file);
if ($strip) {
$content = $this->stripWhitespace($content);
} elseif ('LICENSE' === basename($file)) {
$content = "\n" . $content . "\n";
}
$phar->addFromString($path, $content);
}
/**
* Removes whitespace from a PHP source string while preserving line numbers.
*
* @param string $source A PHP string
*
* @return string The PHP string with the whitespace removed
*/
private function stripWhitespace($source)
{
if (!function_exists('token_get_all')) {
return $source;
}
$output = '';
foreach (token_get_all($source) as $token) {
if (is_string($token)) {
$output .= $token;
} elseif (in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) {
$output .= str_repeat("\n", substr_count($token[1], "\n"));
} elseif (T_WHITESPACE === $token[0]) {
// reduce wide spaces
$whitespace = preg_replace('{[ \t]+}', ' ', $token[1]);
// normalize newlines to \n
$whitespace = preg_replace('{(?:\r\n|\r|\n)}', "\n", $whitespace);
// trim leading spaces
$whitespace = preg_replace('{\n +}', "\n", $whitespace);
$output .= $whitespace;
} else {
$output .= $token[1];
}
}
return $output;
}
private static function getStubLicense()
{
$license = file_get_contents(__DIR__ . '/../../LICENSE');
$license = str_replace('The MIT License (MIT)', '', $license);
$license = str_replace("\n", "\n * ", trim($license));
return $license;
}
const STUB_AUTOLOAD = <<<'EOS'
Phar::mapPhar('psysh.phar');
require 'phar://psysh.phar/build-vendor/autoload.php';
EOS;
/**
* Get a Phar stub for psysh.
*
* This is basically the psysh bin, with the autoload require statements swapped out.
*
* @return string
*/
private function getStub()
{
$content = file_get_contents(__DIR__ . '/../../bin/psysh');
if (version_compare(PHP_VERSION, '5.4', '<')) {
$content = str_replace('#!/usr/bin/env php', '#!/usr/bin/env php -d detect_unicode=Off', $content);
}
$content = preg_replace('{/\* <<<.*?>>> \*/}sm', self::STUB_AUTOLOAD, $content);
$content = preg_replace('/\\(c\\) .*?with this source code./sm', self::getStubLicense(), $content);
$content .= '__HALT_COMPILER();';
return $content;
}
}

237
vendor/psy/psysh/src/Psy/ConfigPaths.php vendored Normal file
View file

@ -0,0 +1,237 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy;
use XdgBaseDir\Xdg;
/**
* A Psy Shell configuration path helper.
*/
class ConfigPaths
{
/**
* Get potential config directory paths.
*
* Returns `~/.psysh`, `%APPDATA%/PsySH` (when on Windows), and all
* XDG Base Directory config directories:
*
* http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
*
* @return string[]
*/
public static function getConfigDirs()
{
$xdg = new Xdg();
return self::getDirNames($xdg->getConfigDirs());
}
/**
* Get potential home config directory paths.
*
* Returns `~/.psysh`, `%APPDATA%/PsySH` (when on Windows), and the
* XDG Base Directory home config directory:
*
* http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
*
* @return string[]
*/
public static function getHomeConfigDirs()
{
$xdg = new Xdg();
return self::getDirNames(array($xdg->getHomeConfigDir()));
}
/**
* Get the current home config directory.
*
* Returns the highest precedence home config directory which actually
* exists. If none of them exists, returns the highest precedence home
* config directory (`%APPDATA%/PsySH` on Windows, `~/.config/psysh`
* everywhere else).
*
* @see self::getHomeConfigDirs
*
* @return string
*/
public static function getCurrentConfigDir()
{
$configDirs = self::getHomeConfigDirs();
foreach ($configDirs as $configDir) {
if (@is_dir($configDir)) {
return $configDir;
}
}
return $configDirs[0];
}
/**
* Find real config files in config directories.
*
* @param string[] $names Config file names
* @param string $configDir Optionally use a specific config directory
*
* @return string[]
*/
public static function getConfigFiles(array $names, $configDir = null)
{
$dirs = ($configDir === null) ? self::getConfigDirs() : array($configDir);
return self::getRealFiles($dirs, $names);
}
/**
* Get potential data directory paths.
*
* If a `dataDir` option was explicitly set, returns an array containing
* just that directory.
*
* Otherwise, it returns `~/.psysh` and all XDG Base Directory data directories:
*
* http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
*
* @return string[]
*/
public static function getDataDirs()
{
$xdg = new Xdg();
return self::getDirNames($xdg->getDataDirs());
}
/**
* Find real data files in config directories.
*
* @param string[] $names Config file names
* @param string $dataDir Optionally use a specific config directory
*
* @return string[]
*/
public static function getDataFiles(array $names, $dataDir = null)
{
$dirs = ($dataDir === null) ? self::getDataDirs() : array($dataDir);
return self::getRealFiles($dirs, $names);
}
/**
* Get a runtime directory.
*
* Defaults to `/psysh` inside the system's temp dir.
*
* @return string
*/
public static function getRuntimeDir()
{
$xdg = new Xdg();
set_error_handler(array('Psy\Exception\ErrorException', 'throwException'));
try {
// XDG doesn't really work on Windows, sometimes complains about
// permissions, sometimes tries to remove non-empty directories.
// It's a bit flaky. So we'll give this a shot first...
$runtimeDir = $xdg->getRuntimeDir(false);
} catch (\Exception $e) {
// Well. That didn't work. Fall back to a boring old folder in the
// system temp dir.
$runtimeDir = sys_get_temp_dir();
}
restore_error_handler();
return strtr($runtimeDir, '\\', '/') . '/psysh';
}
private static function getDirNames(array $baseDirs)
{
$dirs = array_map(function ($dir) {
return strtr($dir, '\\', '/') . '/psysh';
}, $baseDirs);
// Add ~/.psysh
if ($home = getenv('HOME')) {
$dirs[] = strtr($home, '\\', '/') . '/.psysh';
}
// Add some Windows specific ones :)
if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
if ($appData = getenv('APPDATA')) {
// AppData gets preference
array_unshift($dirs, strtr($appData, '\\', '/') . '/PsySH');
}
$dir = strtr(getenv('HOMEDRIVE') . '/' . getenv('HOMEPATH'), '\\', '/') . '/.psysh';
if (!in_array($dir, $dirs)) {
$dirs[] = $dir;
}
}
return $dirs;
}
private static function getRealFiles(array $dirNames, array $fileNames)
{
$files = array();
foreach ($dirNames as $dir) {
foreach ($fileNames as $name) {
$file = $dir . '/' . $name;
if (@is_file($file)) {
$files[] = $file;
}
}
}
return $files;
}
/**
* Ensure that $file exists and is writable, make the parent directory if necessary.
*
* Generates E_USER_NOTICE error if either $file or its directory is not writable.
*
* @param string $file
*
* @return string|false Full path to $file, or false if file is not writable
*/
public static function touchFileWithMkdir($file)
{
if (file_exists($file)) {
if (is_writable($file)) {
return $file;
}
trigger_error(sprintf('Writing to %s is not allowed.', $file), E_USER_NOTICE);
return false;
}
$dir = dirname($file);
if (!is_dir($dir)) {
// Just try making it and see if it works
@mkdir($dir, 0700, true);
}
if (!is_dir($dir) || !is_writable($dir)) {
trigger_error(sprintf('Writing to %s is not allowed.', $dir), E_USER_NOTICE);
return false;
}
touch($file);
return $file;
}
}

1298
vendor/psy/psysh/src/Psy/Configuration.php vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,82 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy;
use JakubOnderka\PhpConsoleColor\ConsoleColor;
use JakubOnderka\PhpConsoleHighlighter\Highlighter;
/**
* Builds `ConsoleColor` instances configured according to the given color mode.
*/
class ConsoleColorFactory
{
private $colorMode;
/**
* @param string $colorMode
*/
public function __construct($colorMode)
{
$this->colorMode = $colorMode;
}
/**
* Get a `ConsoleColor` instance configured according to the given color
* mode.
*
* @return ConsoleColor
*/
public function getConsoleColor()
{
if ($this->colorMode === Configuration::COLOR_MODE_AUTO) {
return $this->getDefaultConsoleColor();
} elseif ($this->colorMode === Configuration::COLOR_MODE_FORCED) {
return $this->getForcedConsoleColor();
} elseif ($this->colorMode === Configuration::COLOR_MODE_DISABLED) {
return $this->getDisabledConsoleColor();
}
}
private function getDefaultConsoleColor()
{
$color = new ConsoleColor();
$color->addTheme(Highlighter::LINE_NUMBER, array('blue'));
$color->addTheme(Highlighter::TOKEN_KEYWORD, array('yellow'));
$color->addTheme(Highlighter::TOKEN_STRING, array('green'));
$color->addTheme(Highlighter::TOKEN_COMMENT, array('dark_gray'));
return $color;
}
private function getForcedConsoleColor()
{
$color = $this->getDefaultConsoleColor();
$color->setForceStyle(true);
return $color;
}
private function getDisabledConsoleColor()
{
$color = new ConsoleColor();
$color->addTheme(Highlighter::TOKEN_STRING, array('none'));
$color->addTheme(Highlighter::TOKEN_COMMENT, array('none'));
$color->addTheme(Highlighter::TOKEN_KEYWORD, array('none'));
$color->addTheme(Highlighter::TOKEN_DEFAULT, array('none'));
$color->addTheme(Highlighter::TOKEN_HTML, array('none'));
$color->addTheme(Highlighter::ACTUAL_LINE_MARK, array('none'));
$color->addTheme(Highlighter::LINE_NUMBER, array('none'));
return $color;
}
}

293
vendor/psy/psysh/src/Psy/Context.php vendored Normal file
View file

@ -0,0 +1,293 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy;
/**
* The Shell execution context.
*
* This class encapsulates the current variables, most recent return value and
* exception, and the current namespace.
*/
class Context
{
private static $specialNames = array('_', '_e', '__out', '__psysh__', 'this');
// Whitelist a very limited number of command-scope magic variable names.
// This might be a bad idea, but future me can sort it out.
private static $commandScopeNames = array(
'__function', '__method', '__class', '__namespace', '__file', '__line', '__dir',
);
private $scopeVariables = array();
private $commandScopeVariables = array();
private $returnValue;
private $lastException;
private $lastStdout;
private $boundObject;
/**
* Get a context variable.
*
* @throws InvalidArgumentException If the variable is not found in the current context
*
* @param string $name
*
* @return mixed
*/
public function get($name)
{
switch ($name) {
case '_':
return $this->returnValue;
case '_e':
if (isset($this->lastException)) {
return $this->lastException;
}
break;
case '__out':
if (isset($this->lastStdout)) {
return $this->lastStdout;
}
break;
case 'this':
if (isset($this->boundObject)) {
return $this->boundObject;
}
break;
case '__function':
case '__method':
case '__class':
case '__namespace':
case '__file':
case '__line':
case '__dir':
if (array_key_exists($name, $this->commandScopeVariables)) {
return $this->commandScopeVariables[$name];
}
break;
default:
if (array_key_exists($name, $this->scopeVariables)) {
return $this->scopeVariables[$name];
}
break;
}
throw new \InvalidArgumentException('Unknown variable: $' . $name);
}
/**
* Get all defined variables.
*
* @return array
*/
public function getAll()
{
return array_merge($this->scopeVariables, $this->getSpecialVariables());
}
/**
* Get all defined magic variables: $_, $_e, $__out, $__class, $__file, etc.
*
* @return array
*/
public function getSpecialVariables()
{
$vars = array(
'_' => $this->returnValue,
);
if (isset($this->lastException)) {
$vars['_e'] = $this->lastException;
}
if (isset($this->lastStdout)) {
$vars['__out'] = $this->lastStdout;
}
if (isset($this->boundObject)) {
$vars['this'] = $this->boundObject;
}
return array_merge($vars, $this->commandScopeVariables);
}
/**
* Set all scope variables.
*
* This method does *not* set any of the magic variables: $_, $_e, $__out,
* $__class, $__file, etc.
*
* @param array $vars
*/
public function setAll(array $vars)
{
foreach (self::$specialNames as $key) {
unset($vars[$key]);
}
foreach (self::$commandScopeNames as $key) {
unset($vars[$key]);
}
$this->scopeVariables = $vars;
}
/**
* Set the most recent return value.
*
* @param mixed $value
*/
public function setReturnValue($value)
{
$this->returnValue = $value;
}
/**
* Get the most recent return value.
*
* @return mixed
*/
public function getReturnValue()
{
return $this->returnValue;
}
/**
* Set the most recent Exception.
*
* @param \Exception $e
*/
public function setLastException(\Exception $e)
{
$this->lastException = $e;
}
/**
* Get the most recent Exception.
*
* @throws InvalidArgumentException If no Exception has been caught
*
* @return null|Exception
*/
public function getLastException()
{
if (!isset($this->lastException)) {
throw new \InvalidArgumentException('No most-recent exception');
}
return $this->lastException;
}
/**
* Set the most recent output from evaluated code.
*
* @param string $lastStdout
*/
public function setLastStdout($lastStdout)
{
$this->lastStdout = $lastStdout;
}
/**
* Get the most recent output from evaluated code.
*
* @throws InvalidArgumentException If no output has happened yet
*
* @return null|string
*/
public function getLastStdout()
{
if (!isset($this->lastStdout)) {
throw new \InvalidArgumentException('No most-recent output');
}
return $this->lastStdout;
}
/**
* Set the bound object ($this variable) for the interactive shell.
*
* @param object|null $boundObject
*/
public function setBoundObject($boundObject)
{
$this->boundObject = is_object($boundObject) ? $boundObject : null;
}
/**
* Get the bound object ($this variable) for the interactive shell.
*
* @return object|null
*/
public function getBoundObject()
{
return $this->boundObject;
}
/**
* Set command-scope magic variables: $__class, $__file, etc.
*
* @param array $commandScopeVariables
*/
public function setCommandScopeVariables(array $commandScopeVariables)
{
$vars = array();
foreach ($commandScopeVariables as $key => $value) {
// kind of type check
if (is_scalar($value) && in_array($key, self::$commandScopeNames)) {
$vars[$key] = $value;
}
}
$this->commandScopeVariables = $vars;
}
/**
* Get command-scope magic variables: $__class, $__file, etc.
*
* @return array
*/
public function getCommandScopeVariables()
{
return $this->commandScopeVariables;
}
/**
* Get unused command-scope magic variables names: __class, __file, etc.
*
* This is used by the shell to unset old command-scope variables after a
* new batch is set.
*
* @return array Array of unused variable names
*/
public function getUnusedCommandScopeVariableNames()
{
return array_diff(self::$commandScopeNames, array_keys($this->commandScopeVariables));
}
/**
* Check whether a variable name is a magic variable.
*
* @param string $name
*
* @return bool
*/
public static function isSpecialVariableName($name)
{
return in_array($name, self::$specialNames) || in_array($name, self::$commandScopeNames);
}
}

View file

@ -0,0 +1,28 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy;
/**
* ContextAware interface.
*
* This interface is used to pass the Shell's context into commands and such
* which require access to the current scope variables.
*/
interface ContextAware
{
/**
* Set the Context reference.
*
* @param Context $context
*/
public function setContext(Context $context);
}

View file

@ -0,0 +1,51 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Exception;
/**
* A break exception, used for halting the Psy Shell.
*/
class BreakException extends \Exception implements Exception
{
private $rawMessage;
/**
* {@inheritdoc}
*/
public function __construct($message = '', $code = 0, \Exception $previous = null)
{
$this->rawMessage = $message;
parent::__construct(sprintf('Exit: %s', $message), $code, $previous);
}
/**
* Return a raw (unformatted) version of the error message.
*
* @return string
*/
public function getRawMessage()
{
return $this->rawMessage;
}
/**
* Throws BreakException.
*
* Since `throw` can not be inserted into arbitrary expressions, it wraps with function call.
*
* @throws BreakException
*/
public static function exitShell()
{
throw new self('Goodbye');
}
}

View file

@ -0,0 +1,20 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Exception;
/**
* A DeprecatedException for Psy.
*/
class DeprecatedException extends RuntimeException
{
// This space intentionally left blank.
}

View file

@ -0,0 +1,114 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Exception;
/**
* A custom error Exception for Psy with a formatted $message.
*/
class ErrorException extends \ErrorException implements Exception
{
private $rawMessage;
/**
* Construct a Psy ErrorException.
*
* @param string $message (default: "")
* @param int $code (default: 0)
* @param int $severity (default: 1)
* @param string $filename (default: null)
* @param int $lineno (default: null)
* @param Exception $previous (default: null)
*/
public function __construct($message = '', $code = 0, $severity = 1, $filename = null, $lineno = null, $previous = null)
{
$this->rawMessage = $message;
if (!empty($filename) && preg_match('{Psy[/\\\\]ExecutionLoop}', $filename)) {
$filename = '';
}
switch ($severity) {
case E_STRICT:
$type = 'Strict error';
break;
case E_NOTICE:
case E_USER_NOTICE:
$type = 'Notice';
break;
case E_WARNING:
case E_CORE_WARNING:
case E_COMPILE_WARNING:
case E_USER_WARNING:
$type = 'Warning';
break;
case E_DEPRECATED:
case E_USER_DEPRECATED:
$type = 'Deprecated';
break;
case E_RECOVERABLE_ERROR:
$type = 'Recoverable fatal error';
break;
default:
$type = 'Error';
break;
}
$message = sprintf('PHP %s: %s%s on line %d', $type, $message, $filename ? ' in ' . $filename : '', $lineno);
parent::__construct($message, $code, $severity, $filename, $lineno, $previous);
}
/**
* Get the raw (unformatted) message for this error.
*
* @return string
*/
public function getRawMessage()
{
return $this->rawMessage;
}
/**
* Helper for throwing an ErrorException.
*
* This allows us to:
*
* set_error_handler(array('Psy\Exception\ErrorException', 'throwException'));
*
* @throws ErrorException
*
* @param int $errno Error type
* @param string $errstr Message
* @param string $errfile Filename
* @param int $errline Line number
*/
public static function throwException($errno, $errstr, $errfile, $errline)
{
throw new self($errstr, 0, $errno, $errfile, $errline);
}
/**
* Create an ErrorException from an Error.
*
* @param \Error $e
*
* @return ErrorException
*/
public static function fromError(\Error $e)
{
return new self($e->getMessage(), $e->getCode(), 1, $e->getFile(), $e->getLine(), $e);
}
}

View file

@ -0,0 +1,27 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Exception;
/**
* An interface for Psy Exceptions.
*/
interface Exception
{
/**
* This is the only thing, really...
*
* Return a raw (unformatted) version of the message.
*
* @return string
*/
public function getRawMessage();
}

View file

@ -0,0 +1,52 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Exception;
/**
* A "fatal error" Exception for Psy.
*/
class FatalErrorException extends \ErrorException implements Exception
{
private $rawMessage;
/**
* Create a fatal error.
*
* @param string $message (default: "")
* @param int $code (default: 0)
* @param int $severity (default: 1)
* @param string $filename (default: null)
* @param int $lineno (default: null)
* @param \Exception $previous (default: null)
*/
public function __construct($message = '', $code = 0, $severity = 1, $filename = null, $lineno = null, $previous = null)
{
// Since these are basically always PHP Parser Node line numbers, treat -1 as null.
if ($lineno === -1) {
$lineno = null;
}
$this->rawMessage = $message;
$message = sprintf('PHP Fatal error: %s in %s on line %d', $message, $filename ?: "eval()'d code", $lineno);
parent::__construct($message, $code, $severity, $filename, $lineno, $previous);
}
/**
* Return a raw (unformatted) version of the error message.
*
* @return string
*/
public function getRawMessage()
{
return $this->rawMessage;
}
}

View file

@ -0,0 +1,42 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Exception;
/**
* A "parse error" Exception for Psy.
*/
class ParseErrorException extends \PhpParser\Error implements Exception
{
/**
* Constructor!
*
* @param string $message (default: "")
* @param int $line (default: -1)
*/
public function __construct($message = '', $line = -1)
{
$message = sprintf('PHP Parse error: %s', $message);
parent::__construct($message, $line);
}
/**
* Create a ParseErrorException from a PhpParser Error.
*
* @param \PhpParser\Error $e
*
* @return ParseErrorException
*/
public static function fromParseError(\PhpParser\Error $e)
{
return new self($e->getRawMessage(), $e->getStartLine());
}
}

View file

@ -0,0 +1,43 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Exception;
/**
* A RuntimeException for Psy.
*/
class RuntimeException extends \RuntimeException implements Exception
{
private $rawMessage;
/**
* Make this bad boy.
*
* @param string $message (default: "")
* @param int $code (default: 0)
* @param \Exception $previous (default: null)
*/
public function __construct($message = '', $code = 0, \Exception $previous = null)
{
$this->rawMessage = $message;
parent::__construct($message, $code, $previous);
}
/**
* Return a raw (unformatted) version of the error message.
*
* @return string
*/
public function getRawMessage()
{
return $this->rawMessage;
}
}

View file

@ -0,0 +1,37 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Exception;
/**
* A throw-up exception, used for throwing an exception out of the Psy Shell.
*/
class ThrowUpException extends \Exception implements Exception
{
/**
* {@inheritdoc}
*/
public function __construct(\Exception $exception)
{
$message = sprintf("Throwing %s with message '%s'", get_class($exception), $exception->getMessage());
parent::__construct($message, $exception->getCode(), $exception);
}
/**
* Return a raw (unformatted) version of the error message.
*
* @return string
*/
public function getRawMessage()
{
return $this->getPrevious()->getMessage();
}
}

View file

@ -0,0 +1,55 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Exception;
/**
* A "type error" Exception for Psy.
*/
class TypeErrorException extends \Exception implements Exception
{
private $rawMessage;
/**
* Constructor!
*
* @param string $message (default: "")
* @param int $code (default: 0)
*/
public function __construct($message = '', $code = 0)
{
$this->rawMessage = $message;
$message = preg_replace('/, called in .*?: eval\\(\\)\'d code/', '', $message);
parent::__construct(sprintf('TypeError: %s', $message), $code);
}
/**
* Get the raw (unformatted) message for this error.
*
* @return string
*/
public function getRawMessage()
{
return $this->rawMessage;
}
/**
* Create a TypeErrorException from a TypeError.
*
* @param \TypeError $e
*
* @return TypeErrorException
*/
public static function fromTypeError(\TypeError $e)
{
return new self($e->getMessage(), $e->getLine());
}
}

View file

@ -0,0 +1,177 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\ExecutionLoop;
use Psy\Context;
use Psy\Shell;
/**
* A forking version of the Psy Shell execution loop.
*
* This version is preferred, as it won't die prematurely if user input includes
* a fatal error, such as redeclaring a class or function.
*/
class ForkingLoop extends Loop
{
private $savegame;
/**
* Run the execution loop.
*
* Forks into a master and a loop process. The loop process will handle the
* evaluation of all instructions, then return its state via a socket upon
* completion.
*
* @param Shell $shell
*/
public function run(Shell $shell)
{
list($up, $down) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
if (!$up) {
throw new \RuntimeException('Unable to create socket pair.');
}
$pid = pcntl_fork();
if ($pid < 0) {
throw new \RuntimeException('Unable to start execution loop.');
} elseif ($pid > 0) {
// This is the main thread. We'll just wait for a while.
// We won't be needing this one.
fclose($up);
// Wait for a return value from the loop process.
$read = array($down);
$write = null;
$except = null;
if (stream_select($read, $write, $except, null) === false) {
throw new \RuntimeException('Error waiting for execution loop.');
}
$content = stream_get_contents($down);
fclose($down);
if ($content) {
$shell->setScopeVariables(@unserialize($content));
}
return;
}
// This is the child process. It's going to do all the work.
if (function_exists('setproctitle')) {
setproctitle('psysh (loop)');
}
// We won't be needing this one.
fclose($down);
// Let's do some processing.
parent::run($shell);
// Send the scope variables back up to the main thread
fwrite($up, $this->serializeReturn($shell->getScopeVariables(false)));
fclose($up);
posix_kill(posix_getpid(), SIGKILL);
}
/**
* Create a savegame at the start of each loop iteration.
*/
public function beforeLoop()
{
$this->createSavegame();
}
/**
* Clean up old savegames at the end of each loop iteration.
*/
public function afterLoop()
{
// if there's an old savegame hanging around, let's kill it.
if (isset($this->savegame)) {
posix_kill($this->savegame, SIGKILL);
pcntl_signal_dispatch();
}
}
/**
* Create a savegame fork.
*
* The savegame contains the current execution state, and can be resumed in
* the event that the worker dies unexpectedly (for example, by encountering
* a PHP fatal error).
*/
private function createSavegame()
{
// the current process will become the savegame
$this->savegame = posix_getpid();
$pid = pcntl_fork();
if ($pid < 0) {
throw new \RuntimeException('Unable to create savegame fork.');
} elseif ($pid > 0) {
// we're the savegame now... let's wait and see what happens
pcntl_waitpid($pid, $status);
// worker exited cleanly, let's bail
if (!pcntl_wexitstatus($status)) {
posix_kill(posix_getpid(), SIGKILL);
}
// worker didn't exit cleanly, we'll need to have another go
$this->createSavegame();
}
}
/**
* Serialize all serializable return values.
*
* A naïve serialization will run into issues if there is a Closure or
* SimpleXMLElement (among other things) in scope when exiting the execution
* loop. We'll just ignore these unserializable classes, and serialize what
* we can.
*
* @param array $return
*
* @return string
*/
private function serializeReturn(array $return)
{
$serializable = array();
foreach ($return as $key => $value) {
// No need to return magic variables
if (Context::isSpecialVariableName($key)) {
continue;
}
// Resources and Closures don't error, but they don't serialize well either.
if (is_resource($value) || $value instanceof \Closure) {
continue;
}
try {
@serialize($value);
$serializable[$key] = $value;
} catch (\Exception $e) {
// we'll just ignore this one...
} catch (\Throwable $e) {
// and this one too...
}
}
return @serialize($serializable);
}
}

View file

@ -0,0 +1,186 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\ExecutionLoop;
use Psy\Configuration;
use Psy\Exception\BreakException;
use Psy\Exception\ErrorException;
use Psy\Exception\ThrowUpException;
use Psy\Exception\TypeErrorException;
use Psy\Shell;
/**
* The Psy Shell execution loop.
*/
class Loop
{
const NOOP_INPUT = 'return null;';
/**
* Loop constructor.
*
* The non-forking loop doesn't have much use for Configuration, so we'll
* just ignore it.
*
* @param Configuration $config
*/
public function __construct(Configuration $config)
{
// don't need this
}
/**
* Run the execution loop.
*
* @throws ThrowUpException if thrown by the `throw-up` command
*
* @param Shell $shell
*/
public function run(Shell $shell)
{
$loop = function ($__psysh__) {
// Load user-defined includes
set_error_handler(array($__psysh__, 'handleError'));
try {
foreach ($__psysh__->getIncludes() as $__psysh_include__) {
include $__psysh_include__;
}
} catch (\Exception $_e) {
$__psysh__->writeException($_e);
}
restore_error_handler();
unset($__psysh_include__);
extract($__psysh__->getScopeVariables(false));
do {
$__psysh__->beforeLoop();
$__psysh__->setScopeVariables(get_defined_vars());
try {
// read a line, see if we should eval
$__psysh__->getInput();
// evaluate the current code buffer
ob_start(
array($__psysh__, 'writeStdout'),
version_compare(PHP_VERSION, '5.4', '>=') ? 1 : 2
);
// Let PsySH inject some magic variables back into the
// shell scope... things like $__class, and $__file set by
// reflection commands
extract($__psysh__->getSpecialScopeVariables(false));
// And unset any magic variables which are no longer needed
foreach ($__psysh__->getUnusedCommandScopeVariableNames() as $__psysh_var_name__) {
unset($$__psysh_var_name__, $__psysh_var_name__);
}
set_error_handler(array($__psysh__, 'handleError'));
$_ = eval($__psysh__->flushCode() ?: Loop::NOOP_INPUT);
restore_error_handler();
ob_end_flush();
$__psysh__->writeReturnValue($_);
} catch (BreakException $_e) {
restore_error_handler();
if (ob_get_level() > 0) {
ob_end_clean();
}
$__psysh__->writeException($_e);
return;
} catch (ThrowUpException $_e) {
restore_error_handler();
if (ob_get_level() > 0) {
ob_end_clean();
}
$__psysh__->writeException($_e);
throw $_e;
} catch (\TypeError $_e) {
restore_error_handler();
if (ob_get_level() > 0) {
ob_end_clean();
}
$__psysh__->writeException(TypeErrorException::fromTypeError($_e));
} catch (\Error $_e) {
restore_error_handler();
if (ob_get_level() > 0) {
ob_end_clean();
}
$__psysh__->writeException(ErrorException::fromError($_e));
} catch (\Exception $_e) {
restore_error_handler();
if (ob_get_level() > 0) {
ob_end_clean();
}
$__psysh__->writeException($_e);
}
$__psysh__->afterLoop();
} while (true);
};
// bind the closure to $this from the shell scope variables...
if (self::bindLoop()) {
$that = $shell->getBoundObject();
if (is_object($that)) {
$loop = $loop->bindTo($that, get_class($that));
} else {
$loop = $loop->bindTo(null, null);
}
}
$loop($shell);
}
/**
* A beforeLoop callback.
*
* This is executed at the start of each loop iteration. In the default
* (non-forking) loop implementation, this is a no-op.
*/
public function beforeLoop()
{
// no-op
}
/**
* A afterLoop callback.
*
* This is executed at the end of each loop iteration. In the default
* (non-forking) loop implementation, this is a no-op.
*/
public function afterLoop()
{
// no-op
}
/**
* Decide whether to bind the execution loop.
*
* @return bool
*/
protected static function bindLoop()
{
// skip binding on HHVM <= 3.5.0
// see https://github.com/facebook/hhvm/issues/1203
if (defined('HHVM_VERSION')) {
return version_compare(HHVM_VERSION, '3.5.0', '>=');
}
return version_compare(PHP_VERSION, '5.4', '>=');
}
}

View file

@ -0,0 +1,76 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Formatter;
use JakubOnderka\PhpConsoleHighlighter\Highlighter;
use Psy\Configuration;
use Psy\ConsoleColorFactory;
use Psy\Exception\RuntimeException;
/**
* A pretty-printer for code.
*/
class CodeFormatter implements Formatter
{
/**
* Format the code represented by $reflector.
*
* @param \Reflector $reflector
* @param null|string $colorMode (default: null)
*
* @return string formatted code
*/
public static function format(\Reflector $reflector, $colorMode = null)
{
if (!self::isReflectable($reflector)) {
throw new RuntimeException('Source code unavailable.');
}
$colorMode = $colorMode ?: Configuration::COLOR_MODE_AUTO;
if ($reflector instanceof \ReflectionGenerator) {
$reflector = $reflector->getFunction();
}
if ($fileName = $reflector->getFileName()) {
if (!is_file($fileName)) {
throw new RuntimeException('Source code unavailable.');
}
$file = file_get_contents($fileName);
$start = $reflector->getStartLine();
$end = $reflector->getEndLine() - $start;
$factory = new ConsoleColorFactory($colorMode);
$colors = $factory->getConsoleColor();
$highlighter = new Highlighter($colors);
return $highlighter->getCodeSnippet($file, $start, 0, $end);
} else {
throw new RuntimeException('Source code unavailable.');
}
}
/**
* Check whether a Reflector instance is reflectable by this formatter.
*
* @param \Reflector $reflector
*
* @return bool
*/
private static function isReflectable(\Reflector $reflector)
{
return $reflector instanceof \ReflectionClass ||
$reflector instanceof \ReflectionFunctionAbstract ||
$reflector instanceof \ReflectionGenerator;
}
}

View file

@ -0,0 +1,168 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Formatter;
use Psy\Util\Docblock;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* A pretty-printer for docblocks.
*/
class DocblockFormatter implements Formatter
{
private static $vectorParamTemplates = array(
'type' => 'info',
'var' => 'strong',
);
/**
* Format a docblock.
*
* @param \Reflector $reflector
*
* @return string Formatted docblock
*/
public static function format(\Reflector $reflector)
{
$docblock = new Docblock($reflector);
$chunks = array();
if (!empty($docblock->desc)) {
$chunks[] = '<comment>Description:</comment>';
$chunks[] = self::indent(OutputFormatter::escape($docblock->desc), ' ');
$chunks[] = '';
}
if (!empty($docblock->tags)) {
foreach ($docblock::$vectors as $name => $vector) {
if (isset($docblock->tags[$name])) {
$chunks[] = sprintf('<comment>%s:</comment>', self::inflect($name));
$chunks[] = self::formatVector($vector, $docblock->tags[$name]);
$chunks[] = '';
}
}
$tags = self::formatTags(array_keys($docblock::$vectors), $docblock->tags);
if (!empty($tags)) {
$chunks[] = $tags;
$chunks[] = '';
}
}
return rtrim(implode("\n", $chunks));
}
/**
* Format a docblock vector, for example, `@throws`, `@param`, or `@return`.
*
* @see DocBlock::$vectors
*
* @param array $vector
* @param array $lines
*
* @return string
*/
private static function formatVector(array $vector, array $lines)
{
$template = array(' ');
foreach ($vector as $type) {
$max = 0;
foreach ($lines as $line) {
$chunk = $line[$type];
$cur = empty($chunk) ? 0 : strlen($chunk) + 1;
if ($cur > $max) {
$max = $cur;
}
}
$template[] = self::getVectorParamTemplate($type, $max);
}
$template = implode(' ', $template);
return implode("\n", array_map(function ($line) use ($template) {
$escaped = array_map(array('Symfony\Component\Console\Formatter\OutputFormatter', 'escape'), $line);
return rtrim(vsprintf($template, $escaped));
}, $lines));
}
/**
* Format docblock tags.
*
* @param array $skip Tags to exclude
* @param array $tags Tags to format
*
* @return string formatted tags
*/
private static function formatTags(array $skip, array $tags)
{
$chunks = array();
foreach ($tags as $name => $values) {
if (in_array($name, $skip)) {
continue;
}
foreach ($values as $value) {
$chunks[] = sprintf('<comment>%s%s</comment> %s', self::inflect($name), empty($value) ? '' : ':', OutputFormatter::escape($value));
}
$chunks[] = '';
}
return implode("\n", $chunks);
}
/**
* Get a docblock vector template.
*
* @param string $type Vector type
* @param int $max Pad width
*
* @return string
*/
private static function getVectorParamTemplate($type, $max)
{
if (!isset(self::$vectorParamTemplates[$type])) {
return sprintf('%%-%ds', $max);
}
return sprintf('<%s>%%-%ds</%s>', self::$vectorParamTemplates[$type], $max, self::$vectorParamTemplates[$type]);
}
/**
* Indent a string.
*
* @param string $text String to indent
* @param string $indent (default: ' ')
*
* @return string
*/
private static function indent($text, $indent = ' ')
{
return $indent . str_replace("\n", "\n" . $indent, $text);
}
/**
* Convert underscored or whitespace separated words into sentence case.
*
* @param string $text
*
* @return string
*/
private static function inflect($text)
{
$words = trim(preg_replace('/[\s_-]+/', ' ', preg_replace('/([a-z])([A-Z])/', '$1 $2', $text)));
return implode(' ', array_map('ucfirst', explode(' ', $words)));
}
}

View file

@ -0,0 +1,25 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Formatter;
/**
* Formatter interface.
*/
interface Formatter
{
/**
* @param \Reflector $reflector
*
* @return string
*/
public static function format(\Reflector $reflector);
}

View file

@ -0,0 +1,273 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Formatter;
use Psy\Reflection\ReflectionConstant;
use Psy\Reflection\ReflectionLanguageConstruct;
use Psy\Util\Json;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* An abstract representation of a function, class or property signature.
*/
class SignatureFormatter implements Formatter
{
/**
* Format a signature for the given reflector.
*
* Defers to subclasses to do the actual formatting.
*
* @param \Reflector $reflector
*
* @return string Formatted signature
*/
public static function format(\Reflector $reflector)
{
switch (true) {
case $reflector instanceof \ReflectionFunction:
case $reflector instanceof ReflectionLanguageConstruct:
return self::formatFunction($reflector);
// this case also covers \ReflectionObject:
case $reflector instanceof \ReflectionClass:
return self::formatClass($reflector);
case $reflector instanceof ReflectionConstant:
return self::formatConstant($reflector);
case $reflector instanceof \ReflectionMethod:
return self::formatMethod($reflector);
case $reflector instanceof \ReflectionProperty:
return self::formatProperty($reflector);
default:
throw new \InvalidArgumentException('Unexpected Reflector class: ' . get_class($reflector));
}
}
/**
* Print the signature name.
*
* @param \Reflector $reflector
*
* @return string Formatted name
*/
public static function formatName(\Reflector $reflector)
{
return $reflector->getName();
}
/**
* Print the method, property or class modifiers.
*
* Technically this should be a trait. Can't wait for 5.4 :)
*
* @param \Reflector $reflector
*
* @return string Formatted modifiers
*/
private static function formatModifiers(\Reflector $reflector)
{
return implode(' ', array_map(function ($modifier) {
return sprintf('<keyword>%s</keyword>', $modifier);
}, \Reflection::getModifierNames($reflector->getModifiers())));
}
/**
* Format a class signature.
*
* @param \ReflectionClass $reflector
*
* @return string Formatted signature
*/
private static function formatClass(\ReflectionClass $reflector)
{
$chunks = array();
if ($modifiers = self::formatModifiers($reflector)) {
$chunks[] = $modifiers;
}
if (version_compare(PHP_VERSION, '5.4', '>=') && $reflector->isTrait()) {
$chunks[] = 'trait';
} else {
$chunks[] = $reflector->isInterface() ? 'interface' : 'class';
}
$chunks[] = sprintf('<class>%s</class>', self::formatName($reflector));
if ($parent = $reflector->getParentClass()) {
$chunks[] = 'extends';
$chunks[] = sprintf('<class>%s</class>', $parent->getName());
}
$interfaces = $reflector->getInterfaceNames();
if (!empty($interfaces)) {
sort($interfaces);
$chunks[] = 'implements';
$chunks[] = implode(', ', array_map(function ($name) {
return sprintf('<class>%s</class>', $name);
}, $interfaces));
}
return implode(' ', $chunks);
}
/**
* Format a constant signature.
*
* @param ReflectionConstant $reflector
*
* @return string Formatted signature
*/
private static function formatConstant(ReflectionConstant $reflector)
{
$value = $reflector->getValue();
$style = self::getTypeStyle($value);
return sprintf(
'<keyword>const</keyword> <const>%s</const> = <%s>%s</%s>',
self::formatName($reflector),
$style,
OutputFormatter::escape(Json::encode($value)),
$style
);
}
/**
* Helper for getting output style for a given value's type.
*
* @param mixed $value
*
* @return string
*/
private static function getTypeStyle($value)
{
if (is_int($value) || is_float($value)) {
return 'number';
} elseif (is_string($value)) {
return 'string';
} elseif (is_bool($value) || is_null($value)) {
return 'bool';
} else {
return 'strong';
}
}
/**
* Format a property signature.
*
* @param \ReflectionProperty $reflector
*
* @return string Formatted signature
*/
private static function formatProperty(\ReflectionProperty $reflector)
{
return sprintf(
'%s <strong>$%s</strong>',
self::formatModifiers($reflector),
$reflector->getName()
);
}
/**
* Format a function signature.
*
* @param \ReflectionFunction $reflector
*
* @return string Formatted signature
*/
private static function formatFunction(\ReflectionFunctionAbstract $reflector)
{
return sprintf(
'<keyword>function</keyword> %s<function>%s</function>(%s)',
$reflector->returnsReference() ? '&' : '',
self::formatName($reflector),
implode(', ', self::formatFunctionParams($reflector))
);
}
/**
* Format a method signature.
*
* @param \ReflectionMethod $reflector
*
* @return string Formatted signature
*/
private static function formatMethod(\ReflectionMethod $reflector)
{
return sprintf(
'%s %s',
self::formatModifiers($reflector),
self::formatFunction($reflector)
);
}
/**
* Print the function params.
*
* @param \ReflectionFunctionAbstract $reflector
*
* @return string
*/
private static function formatFunctionParams(\ReflectionFunctionAbstract $reflector)
{
$params = array();
foreach ($reflector->getParameters() as $param) {
$hint = '';
try {
if ($param->isArray()) {
$hint = '<keyword>array</keyword> ';
} elseif ($class = $param->getClass()) {
$hint = sprintf('<class>%s</class> ', $class->getName());
}
} catch (\Exception $e) {
// sometimes we just don't know...
// bad class names, or autoloaded classes that haven't been loaded yet, or whathaveyou.
// come to think of it, the only time I've seen this is with the intl extension.
// Hax: we'll try to extract it :P
$chunks = explode('$' . $param->getName(), (string) $param);
$chunks = explode(' ', trim($chunks[0]));
$guess = end($chunks);
$hint = sprintf('<urgent>%s</urgent> ', $guess);
}
if ($param->isOptional()) {
if (!$param->isDefaultValueAvailable()) {
$value = 'unknown';
$typeStyle = 'urgent';
} else {
$value = $param->getDefaultValue();
$typeStyle = self::getTypeStyle($value);
$value = is_array($value) ? 'array()' : is_null($value) ? 'null' : var_export($value, true);
}
$default = sprintf(' = <%s>%s</%s>', $typeStyle, OutputFormatter::escape($value), $typeStyle);
} else {
$default = '';
}
$params[] = sprintf(
'%s%s<strong>$%s</strong>%s',
$param->isPassedByReference() ? '&' : '',
$hint,
$param->getName(),
$default
);
}
return $params;
}
}

View file

@ -0,0 +1,50 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Input;
use Symfony\Component\Console\Input\InputArgument;
/**
* An input argument for code.
*
* A CodeArgument must be the final argument of the command. Once all options
* and other arguments are used, any remaining input until the end of the string
* is considered part of a single CodeArgument, regardless of spaces, quoting,
* escaping, etc.
*
* This means commands can support crazy input like
*
* parse function() { return "wheee\n"; }
*
* ... without having to put the code in a quoted string and escape everything.
*/
class CodeArgument extends InputArgument
{
/**
* Constructor.
*
* @param string $name The argument name
* @param int $mode The argument mode: self::REQUIRED or self::OPTIONAL
* @param string $description A description text
* @param mixed $default The default value (for self::OPTIONAL mode only)
*
* @throws \InvalidArgumentException When argument mode is not valid
*/
public function __construct($name, $mode = null, $description = '', $default = null)
{
if ($mode & InputArgument::IS_ARRAY) {
throw new \InvalidArgumentException('Argument mode IS_ARRAY is not valid.');
}
parent::__construct($name, $mode, $description, $default);
}
}

View file

@ -0,0 +1,143 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Input;
use Psy\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
/**
* Parse, validate and match --grep, --insensitive and --invert command options.
*/
class FilterOptions
{
private $filter = false;
private $pattern;
private $insensitive;
private $invert;
/**
* Get input option definitions for filtering.
*
* @return InputOption[]
*/
public static function getOptions()
{
return array(
new InputOption('grep', 'G', InputOption::VALUE_REQUIRED, 'Limit to items matching the given pattern (string or regex).'),
new InputOption('insensitive', 'i', InputOption::VALUE_NONE, 'Case-insensitive search (requires --grep).'),
new InputOption('invert', 'v', InputOption::VALUE_NONE, 'Inverted search (requires --grep).'),
);
}
/**
* Bind input and prepare filter.
*
* @param InputInterface $input
*/
public function bind(InputInterface $input)
{
$this->validateInput($input);
if (!$pattern = $input->getOption('grep')) {
$this->filter = false;
return;
}
if (!$this->stringIsRegex($pattern)) {
$pattern = '/' . preg_quote($pattern, '/') . '/';
}
if ($insensitive = $input->getOption('insensitive')) {
$pattern .= 'i';
}
$this->validateRegex($pattern);
$this->filter = true;
$this->pattern = $pattern;
$this->insensitive = $insensitive;
$this->invert = $input->getOption('invert');
}
/**
* Check whether the bound input has filter options.
*
* @return bool
*/
public function hasFilter()
{
return $this->filter;
}
/**
* Check whether a string matches the current filter options.
*
* @param string $string
* @param array $matches
*
* @return bool
*/
public function match($string, array &$matches = null)
{
return $this->filter === false || (preg_match($this->pattern, $string, $matches) xor $this->invert);
}
/**
* Validate that grep, invert and insensitive input options are consistent.
*
* @param InputInterface $input
*
* @return bool
*/
private function validateInput(InputInterface $input)
{
if (!$input->getOption('grep')) {
foreach (array('invert', 'insensitive') as $option) {
if ($input->getOption($option)) {
throw new RuntimeException('--' . $option . ' does not make sense without --grep');
}
}
}
}
/**
* Check whether a string appears to be a regular expression.
*
* @param string $string
*
* @return bool
*/
private function stringIsRegex($string)
{
return substr($string, 0, 1) === '/' && substr($string, -1) === '/' && strlen($string) >= 3;
}
/**
* Validate that $pattern is a valid regular expression.
*
* @param string $pattern
*
* @return bool
*/
private function validateRegex($pattern)
{
set_error_handler(array('Psy\Exception\ErrorException', 'throwException'));
try {
preg_match($pattern, '');
} catch (ErrorException $e) {
throw new RuntimeException(str_replace('preg_match(): ', 'Invalid regular expression: ', $e->getRawMessage()));
}
restore_error_handler();
}
}

View file

@ -0,0 +1,326 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Input;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\StringInput;
/**
* A StringInput subclass specialized for code arguments.
*/
class ShellInput extends StringInput
{
private $hasCodeArgument = false;
/**
* Unlike the parent implementation's tokens, this contains an array of
* token/rest pairs, so that code arguments can be handled while parsing.
*/
private $tokenPairs;
private $parsed;
/**
* Constructor.
*
* @param string $input An array of parameters from the CLI (in the argv format)
*/
public function __construct($input)
{
parent::__construct($input);
$this->tokenPairs = $this->tokenize($input);
}
/**
* {@inheritdoc}
*
* @throws \InvalidArgumentException if $definition has CodeArgument before the final argument position
*/
public function bind(InputDefinition $definition)
{
$hasCodeArgument = false;
if ($definition->getArgumentCount() > 0) {
$args = $definition->getArguments();
$lastArg = array_pop($args);
foreach ($args as $arg) {
if ($arg instanceof CodeArgument) {
$msg = sprintf('Unexpected CodeArgument before the final position: %s', $arg->getName());
throw new \InvalidArgumentException($msg);
}
}
if ($lastArg instanceof CodeArgument) {
$hasCodeArgument = true;
}
}
$this->hasCodeArgument = $hasCodeArgument;
return parent::bind($definition);
}
/**
* Tokenizes a string.
*
* The version of this on StringInput is good, but doesn't handle code
* arguments if they're at all complicated. This does :)
*
* @param string $input The input to tokenize
*
* @return array An array of token/rest pairs
*
* @throws \InvalidArgumentException When unable to parse input (should never happen)
*/
private function tokenize($input)
{
$tokens = array();
$length = strlen($input);
$cursor = 0;
while ($cursor < $length) {
if (preg_match('/\s+/A', $input, $match, null, $cursor)) {
} elseif (preg_match('/([^="\'\s]+?)(=?)(' . StringInput::REGEX_QUOTED_STRING . '+)/A', $input, $match, null, $cursor)) {
$tokens[] = array(
$match[1] . $match[2] . stripcslashes(str_replace(array('"\'', '\'"', '\'\'', '""'), '', substr($match[3], 1, strlen($match[3]) - 2))),
substr($input, $cursor),
);
} elseif (preg_match('/' . StringInput::REGEX_QUOTED_STRING . '/A', $input, $match, null, $cursor)) {
$tokens[] = array(
stripcslashes(substr($match[0], 1, strlen($match[0]) - 2)),
substr($input, $cursor),
);
} elseif (preg_match('/' . StringInput::REGEX_STRING . '/A', $input, $match, null, $cursor)) {
$tokens[] = array(
stripcslashes($match[1]),
substr($input, $cursor),
);
} else {
// should never happen
throw new \InvalidArgumentException(sprintf('Unable to parse input near "... %s ..."', substr($input, $cursor, 10)));
}
$cursor += strlen($match[0]);
}
return $tokens;
}
/**
* Same as parent, but with some bonus handling for code arguments.
*/
protected function parse()
{
$parseOptions = true;
$this->parsed = $this->tokenPairs;
while (null !== $tokenPair = array_shift($this->parsed)) {
// token is what you'd expect. rest is the remainder of the input
// string, including token, and will be used if this is a code arg.
list($token, $rest) = $tokenPair;
if ($parseOptions && '' === $token) {
$this->parseShellArgument($token, $rest);
} elseif ($parseOptions && '--' === $token) {
$parseOptions = false;
} elseif ($parseOptions && 0 === strpos($token, '--')) {
$this->parseLongOption($token);
} elseif ($parseOptions && '-' === $token[0] && '-' !== $token) {
$this->parseShortOption($token);
} else {
$this->parseShellArgument($token, $rest);
}
}
}
/**
* Parses an argument, with bonus handling for code arguments.
*
* @param string $token The current token
* @param string $rest The remaining unparsed input, including the current token
*
* @throws \RuntimeException When too many arguments are given
*/
private function parseShellArgument($token, $rest)
{
$c = count($this->arguments);
// if input is expecting another argument, add it
if ($this->definition->hasArgument($c)) {
$arg = $this->definition->getArgument($c);
if ($arg instanceof CodeArgument) {
// When we find a code argument, we're done parsing. Add the
// remaining input to the current argument and call it a day.
$this->parsed = array();
$this->arguments[$arg->getName()] = $rest;
} else {
$this->arguments[$arg->getName()] = $arg->isArray() ? array($token) : $token;
}
return;
}
// if last argument isArray(), append token to last argument
if ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) {
$arg = $this->definition->getArgument($c - 1);
$this->arguments[$arg->getName()][] = $token;
return;
}
// unexpected argument
$all = $this->definition->getArguments();
if (count($all)) {
throw new \RuntimeException(sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all))));
}
throw new \RuntimeException(sprintf('No arguments expected, got "%s".', $token));
}
// Everything below this is copypasta from ArgvInput private methods
/**
* Parses a short option.
*
* @param string $token The current token
*/
private function parseShortOption($token)
{
$name = substr($token, 1);
if (strlen($name) > 1) {
if ($this->definition->hasShortcut($name[0]) && $this->definition->getOptionForShortcut($name[0])->acceptValue()) {
// an option with a value (with no space)
$this->addShortOption($name[0], substr($name, 1));
} else {
$this->parseShortOptionSet($name);
}
} else {
$this->addShortOption($name, null);
}
}
/**
* Parses a short option set.
*
* @param string $name The current token
*
* @throws \RuntimeException When option given doesn't exist
*/
private function parseShortOptionSet($name)
{
$len = strlen($name);
for ($i = 0; $i < $len; $i++) {
if (!$this->definition->hasShortcut($name[$i])) {
throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $name[$i]));
}
$option = $this->definition->getOptionForShortcut($name[$i]);
if ($option->acceptValue()) {
$this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1));
break;
} else {
$this->addLongOption($option->getName(), null);
}
}
}
/**
* Parses a long option.
*
* @param string $token The current token
*/
private function parseLongOption($token)
{
$name = substr($token, 2);
if (false !== $pos = strpos($name, '=')) {
if (0 === strlen($value = substr($name, $pos + 1))) {
// if no value after "=" then substr() returns "" since php7 only, false before
// see http://php.net/manual/fr/migration70.incompatible.php#119151
if (PHP_VERSION_ID < 70000 && false === $value) {
$value = '';
}
array_unshift($this->parsed, array($value, null));
}
$this->addLongOption(substr($name, 0, $pos), $value);
} else {
$this->addLongOption($name, null);
}
}
/**
* Adds a short option value.
*
* @param string $shortcut The short option key
* @param mixed $value The value for the option
*
* @throws \RuntimeException When option given doesn't exist
*/
private function addShortOption($shortcut, $value)
{
if (!$this->definition->hasShortcut($shortcut)) {
throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut));
}
$this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
}
/**
* Adds a long option value.
*
* @param string $name The long option key
* @param mixed $value The value for the option
*
* @throws \RuntimeException When option given doesn't exist
*/
private function addLongOption($name, $value)
{
if (!$this->definition->hasOption($name)) {
throw new \RuntimeException(sprintf('The "--%s" option does not exist.', $name));
}
$option = $this->definition->getOption($name);
if (null !== $value && !$option->acceptValue()) {
throw new \RuntimeException(sprintf('The "--%s" option does not accept a value.', $name));
}
if (in_array($value, array('', null), true) && $option->acceptValue() && count($this->parsed)) {
// if option accepts an optional or mandatory argument
// let's see if there is one provided
$next = array_shift($this->parsed);
$nextToken = $next[0];
if ((isset($nextToken[0]) && '-' !== $nextToken[0]) || in_array($nextToken, array('', null), true)) {
$value = $nextToken;
} else {
array_unshift($this->parsed, $next);
}
}
if (null === $value) {
if ($option->isValueRequired()) {
throw new \RuntimeException(sprintf('The "--%s" option requires a value.', $name));
}
if (!$option->isArray() && !$option->isValueOptional()) {
$value = true;
}
}
if ($option->isArray()) {
$this->options[$name][] = $value;
} else {
$this->options[$name] = $value;
}
}
}

View file

@ -0,0 +1,44 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Input;
/**
* A simple class used internally by PsySH to represent silent input.
*
* Silent input is generally used for non-user-generated code, such as the
* rewritten user code run by sudo command. Silent input isn't echoed before
* evaluating, and it's not added to the readline history.
*/
class SilentInput
{
private $inputString;
/**
* Constructor.
*
* @param string $inputString
*/
public function __construct($inputString)
{
$this->inputString = $inputString;
}
/**
* To. String.
*
* @return string
*/
public function __toString()
{
return $this->inputString;
}
}

View file

@ -0,0 +1,26 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Output;
use Symfony\Component\Console\Output\OutputInterface;
/**
* An output pager is much the same as a regular OutputInterface, but allows
* the stream to be flushed to a pager periodically.
*/
interface OutputPager extends OutputInterface
{
/**
* Close the current pager process.
*/
public function close();
}

View file

@ -0,0 +1,39 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Output;
use Symfony\Component\Console\Output\StreamOutput;
/**
* A passthrough pager is a no-op. It simply wraps a StreamOutput's stream and
* does nothing when the pager is closed.
*/
class PassthruPager extends StreamOutput implements OutputPager
{
/**
* Constructor.
*
* @param StreamOutput $output
*/
public function __construct(StreamOutput $output)
{
parent::__construct($output->getStream());
}
/**
* Close the current pager process.
*/
public function close()
{
// nothing to do here
}
}

View file

@ -0,0 +1,103 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Output;
use Symfony\Component\Console\Output\StreamOutput;
/**
* ProcOutputPager class.
*
* A ProcOutputPager instance wraps a regular StreamOutput's stream. Rather
* than writing directly to the stream, it shells out to a pager process and
* gives that process the stream as stdout. This means regular *nix commands
* like `less` and `more` can be used to page large amounts of output.
*/
class ProcOutputPager extends StreamOutput implements OutputPager
{
private $proc;
private $pipe;
private $stream;
private $cmd;
/**
* Constructor.
*
* @param StreamOutput $output
* @param string $cmd Pager process command (default: 'less -R -S -F -X')
*/
public function __construct(StreamOutput $output, $cmd = 'less -R -S -F -X')
{
$this->stream = $output->getStream();
$this->cmd = $cmd;
}
/**
* Writes a message to the output.
*
* @param string $message A message to write to the output
* @param bool $newline Whether to add a newline or not
*
* @throws \RuntimeException When unable to write output (should never happen)
*/
public function doWrite($message, $newline)
{
$pipe = $this->getPipe();
if (false === @fwrite($pipe, $message . ($newline ? PHP_EOL : ''))) {
// @codeCoverageIgnoreStart
// should never happen
throw new \RuntimeException('Unable to write output.');
// @codeCoverageIgnoreEnd
}
fflush($pipe);
}
/**
* Close the current pager process.
*/
public function close()
{
if (isset($this->pipe)) {
fclose($this->pipe);
}
if (isset($this->proc)) {
$exit = proc_close($this->proc);
if ($exit !== 0) {
throw new \RuntimeException('Error closing output stream');
}
}
unset($this->pipe, $this->proc);
}
/**
* Get a pipe for paging output.
*
* If no active pager process exists, fork one and return its input pipe.
*/
private function getPipe()
{
if (!isset($this->pipe) || !isset($this->proc)) {
$desc = array(array('pipe', 'r'), $this->stream, fopen('php://stderr', 'w'));
$this->proc = proc_open($this->cmd, $desc, $pipes);
if (!is_resource($this->proc)) {
throw new \RuntimeException('Error opening output stream');
}
$this->pipe = $pipes[0];
}
return $this->pipe;
}
}

View file

@ -0,0 +1,204 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Output;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Output\ConsoleOutput;
/**
* A ConsoleOutput subclass specifically for Psy Shell output.
*/
class ShellOutput extends ConsoleOutput
{
const NUMBER_LINES = 128;
private $paging = 0;
private $pager;
/**
* Construct a ShellOutput instance.
*
* @param mixed $verbosity (default: self::VERBOSITY_NORMAL)
* @param bool $decorated (default: null)
* @param OutputFormatterInterface $formatter (default: null)
* @param null|string|OutputPager $pager (default: null)
*/
public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = null, OutputFormatterInterface $formatter = null, $pager = null)
{
parent::__construct($verbosity, $decorated, $formatter);
$this->initFormatters();
if ($pager === null) {
$this->pager = new PassthruPager($this);
} elseif (is_string($pager)) {
$this->pager = new ProcOutputPager($this, $pager);
} elseif ($pager instanceof OutputPager) {
$this->pager = $pager;
} else {
throw new \InvalidArgumentException('Unexpected pager parameter: ' . $pager);
}
}
/**
* Page multiple lines of output.
*
* The output pager is started
*
* If $messages is callable, it will be called, passing this output instance
* for rendering. Otherwise, all passed $messages are paged to output.
*
* Upon completion, the output pager is flushed.
*
* @param string|array|\Closure $messages A string, array of strings or a callback
* @param int $type (default: 0)
*/
public function page($messages, $type = 0)
{
if (is_string($messages)) {
$messages = (array) $messages;
}
if (!is_array($messages) && !is_callable($messages)) {
throw new \InvalidArgumentException('Paged output requires a string, array or callback.');
}
$this->startPaging();
if (is_callable($messages)) {
$messages($this);
} else {
$this->write($messages, true, $type);
}
$this->stopPaging();
}
/**
* Start sending output to the output pager.
*/
public function startPaging()
{
$this->paging++;
}
/**
* Stop paging output and flush the output pager.
*/
public function stopPaging()
{
$this->paging--;
$this->closePager();
}
/**
* Writes a message to the output.
*
* Optionally, pass `$type | self::NUMBER_LINES` as the $type parameter to
* number the lines of output.
*
* @throws \InvalidArgumentException When unknown output type is given
*
* @param string|array $messages The message as an array of lines or a single string
* @param bool $newline Whether to add a newline or not
* @param int $type The type of output
*/
public function write($messages, $newline = false, $type = 0)
{
if ($this->getVerbosity() === self::VERBOSITY_QUIET) {
return;
}
$messages = (array) $messages;
if ($type & self::NUMBER_LINES) {
$pad = strlen((string) count($messages));
$template = $this->isDecorated() ? "<aside>%{$pad}s</aside>: %s" : "%{$pad}s: %s";
if ($type & self::OUTPUT_RAW) {
$messages = array_map(array('Symfony\Component\Console\Formatter\OutputFormatter', 'escape'), $messages);
}
foreach ($messages as $i => $line) {
$messages[$i] = sprintf($template, $i, $line);
}
// clean this up for super.
$type = $type & ~self::NUMBER_LINES & ~self::OUTPUT_RAW;
}
parent::write($messages, $newline, $type);
}
/**
* Writes a message to the output.
*
* Handles paged output, or writes directly to the output stream.
*
* @param string $message A message to write to the output
* @param bool $newline Whether to add a newline or not
*/
public function doWrite($message, $newline)
{
if ($this->paging > 0) {
$this->pager->doWrite($message, $newline);
} else {
parent::doWrite($message, $newline);
}
}
/**
* Flush and close the output pager.
*/
private function closePager()
{
if ($this->paging <= 0) {
$this->pager->close();
}
}
/**
* Initialize output formatter styles.
*/
private function initFormatters()
{
$formatter = $this->getFormatter();
$formatter->setStyle('warning', new OutputFormatterStyle('black', 'yellow'));
$formatter->setStyle('error', new OutputFormatterStyle('black', 'red', array('bold')));
$formatter->setStyle('aside', new OutputFormatterStyle('blue'));
$formatter->setStyle('strong', new OutputFormatterStyle(null, null, array('bold')));
$formatter->setStyle('return', new OutputFormatterStyle('cyan'));
$formatter->setStyle('urgent', new OutputFormatterStyle('red'));
$formatter->setStyle('hidden', new OutputFormatterStyle('black'));
// Visibility
$formatter->setStyle('public', new OutputFormatterStyle(null, null, array('bold')));
$formatter->setStyle('protected', new OutputFormatterStyle('yellow'));
$formatter->setStyle('private', new OutputFormatterStyle('red'));
$formatter->setStyle('global', new OutputFormatterStyle('cyan', null, array('bold')));
$formatter->setStyle('const', new OutputFormatterStyle('cyan'));
$formatter->setStyle('class', new OutputFormatterStyle('blue', null, array('underscore')));
$formatter->setStyle('function', new OutputFormatterStyle(null));
$formatter->setStyle('default', new OutputFormatterStyle(null));
// Types
$formatter->setStyle('number', new OutputFormatterStyle('magenta'));
$formatter->setStyle('string', new OutputFormatterStyle('green'));
$formatter->setStyle('bool', new OutputFormatterStyle('cyan'));
$formatter->setStyle('keyword', new OutputFormatterStyle('yellow'));
$formatter->setStyle('comment', new OutputFormatterStyle('blue'));
$formatter->setStyle('object', new OutputFormatterStyle('blue'));
$formatter->setStyle('resource', new OutputFormatterStyle('yellow'));
}
}

View file

@ -0,0 +1,91 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 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\Lexer;
use PhpParser\Parser;
use PhpParser\ParserFactory as OriginalParserFactory;
/**
* Parser factory to abstract over PHP parser library versions.
*/
class ParserFactory
{
const ONLY_PHP5 = 'ONLY_PHP5';
const ONLY_PHP7 = 'ONLY_PHP7';
const PREFER_PHP5 = 'PREFER_PHP5';
const PREFER_PHP7 = 'PREFER_PHP7';
/**
* Possible kinds of parsers for the factory, from PHP parser library.
*
* @return array
*/
public static function getPossibleKinds()
{
return array('ONLY_PHP5', 'ONLY_PHP7', 'PREFER_PHP5', 'PREFER_PHP7');
}
/**
* Is this parser factory supports kinds?
*
* PHP parser < 2.0 doesn't support kinds, >= 2.0 does.
*
* @return bool
*/
public function hasKindsSupport()
{
return class_exists('PhpParser\ParserFactory');
}
/**
* Default kind (if supported, based on current interpreter's version).
*
* @return string|null
*/
public function getDefaultKind()
{
if ($this->hasKindsSupport()) {
return version_compare(PHP_VERSION, '7.0', '>=') ? static::ONLY_PHP7 : static::ONLY_PHP5;
}
}
/**
* New parser instance with given kind.
*
* @param string|null $kind One of class constants (only for PHP parser 2.0 and above)
*
* @return Parser
*/
public function createParser($kind = null)
{
if ($this->hasKindsSupport()) {
$originalFactory = new OriginalParserFactory();
$kind = $kind ?: $this->getDefaultKind();
if (!in_array($kind, static::getPossibleKinds())) {
throw new \InvalidArgumentException('Unknown parser kind');
}
$parser = $originalFactory->create(constant('PhpParser\ParserFactory::' . $kind));
} else {
if ($kind !== null) {
throw new \InvalidArgumentException('Install PHP Parser v2.x to specify parser kind');
}
$parser = new Parser(new Lexer());
}
return $parser;
}
}

View file

@ -0,0 +1,170 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Readline;
/**
* A Readline interface implementation for GNU Readline.
*
* This is by far the coolest way to do it, but it doesn't work with new PHP.
*
* Oh well.
*/
class GNUReadline implements Readline
{
/** @var string|false */
protected $historyFile;
/** @var int */
protected $historySize;
/** @var bool */
protected $eraseDups;
/**
* GNU Readline is supported iff `readline_list_history` is defined. PHP
* decided it would be awesome to swap out GNU Readline for Libedit, but
* they ended up shipping an incomplete implementation. So we've got this.
*
* @return bool
*/
public static function isSupported()
{
return function_exists('readline_list_history');
}
/**
* GNU Readline constructor.
*
* @param string|false $historyFile
* @param int $historySize
* @param bool $eraseDups
*/
public function __construct($historyFile = null, $historySize = 0, $eraseDups = false)
{
$this->historyFile = ($historyFile !== null) ? $historyFile : false;
$this->historySize = $historySize;
$this->eraseDups = $eraseDups;
}
/**
* {@inheritdoc}
*/
public function addHistory($line)
{
if ($res = readline_add_history($line)) {
$this->writeHistory();
}
return $res;
}
/**
* {@inheritdoc}
*/
public function clearHistory()
{
if ($res = readline_clear_history()) {
$this->writeHistory();
}
return $res;
}
/**
* {@inheritdoc}
*/
public function listHistory()
{
return readline_list_history();
}
/**
* {@inheritdoc}
*/
public function readHistory()
{
// Workaround PHP bug #69054
//
// If open_basedir is set, readline_read_history() segfaults. This was fixed in 5.6.7:
//
// https://github.com/php/php-src/blob/423a057023ef3c00d2ffc16a6b43ba01d0f71796/NEWS#L19-L21
//
if (version_compare(PHP_VERSION, '5.6.7', '>=') || !ini_get('open_basedir')) {
readline_read_history();
}
readline_clear_history();
return readline_read_history($this->historyFile);
}
/**
* {@inheritdoc}
*/
public function readline($prompt = null)
{
return readline($prompt);
}
/**
* {@inheritdoc}
*/
public function redisplay()
{
readline_redisplay();
}
/**
* {@inheritdoc}
*/
public function writeHistory()
{
// We have to write history first, since it is used
// by Libedit to list history
if ($this->historyFile !== false) {
$res = readline_write_history($this->historyFile);
} else {
$res = true;
}
if (!$res || !$this->eraseDups && !$this->historySize > 0) {
return $res;
}
$hist = $this->listHistory();
if (!$hist) {
return true;
}
if ($this->eraseDups) {
// flip-flip technique: removes duplicates, latest entries win.
$hist = array_flip(array_flip($hist));
// sort on keys to get the order back
ksort($hist);
}
if ($this->historySize > 0) {
$histsize = count($hist);
if ($histsize > $this->historySize) {
$hist = array_slice($hist, $histsize - $this->historySize);
}
}
readline_clear_history();
foreach ($hist as $line) {
readline_add_history($line);
}
if ($this->historyFile !== false) {
return readline_write_history($this->historyFile);
}
return true;
}
}

View file

@ -0,0 +1,107 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Readline;
use Hoa\Console\Readline\Readline as HoaReadline;
use Psy\Exception\BreakException;
/**
* Hoa\Console Readline implementation.
*/
class HoaConsole implements Readline
{
/** @var HoaReadline */
private $hoaReadline;
/**
* @return bool
*/
public static function isSupported()
{
return class_exists('\Hoa\Console\Console', true);
}
public function __construct()
{
$this->hoaReadline = new HoaReadline();
}
/**
* {@inheritdoc}
*/
public function addHistory($line)
{
$this->hoaReadline->addHistory($line);
return true;
}
/**
* {@inheritdoc}
*/
public function clearHistory()
{
$this->hoaReadline->clearHistory();
return true;
}
/**
* {@inheritdoc}
*/
public function listHistory()
{
$i = 0;
$list = array();
while (($item = $this->hoaReadline->getHistory($i++)) !== null) {
$list[] = $item;
}
return $list;
}
/**
* {@inheritdoc}
*/
public function readHistory()
{
return true;
}
/**
* {@inheritdoc}
*
* @throws BreakException if user hits Ctrl+D
*
* @return string
*/
public function readline($prompt = null)
{
return $this->hoaReadline->readLine($prompt);
}
/**
* {@inheritdoc}
*/
public function redisplay()
{
// noop
}
/**
* {@inheritdoc}
*/
public function writeHistory()
{
return true;
}
}

View file

@ -0,0 +1,83 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Readline;
use Psy\Util\Str;
/**
* A Libedit-based Readline implementation.
*
* This is largely the same as the Readline implementation, but it emulates
* support for `readline_list_history` since PHP decided it was a good idea to
* ship a fake Readline implementation that is missing history support.
*/
class Libedit extends GNUReadline
{
/**
* Let's emulate GNU Readline by manually reading and parsing the history file!
*
* @return bool
*/
public static function isSupported()
{
return function_exists('readline') && !function_exists('readline_list_history');
}
/**
* {@inheritdoc}
*/
public function listHistory()
{
$history = file_get_contents($this->historyFile);
if (!$history) {
return array();
}
// libedit doesn't seem to support non-unix line separators.
$history = explode("\n", $history);
// shift the history signature, ensure it's valid
if (array_shift($history) !== '_HiStOrY_V2_') {
return array();
}
// decode the line
$history = array_map(array($this, 'parseHistoryLine'), $history);
// filter empty lines & comments
return array_values(array_filter($history));
}
/**
* From GNUReadline (readline/histfile.c & readline/histexpand.c):
* lines starting with "\0" are comments or timestamps;
* if "\0" is found in an entry,
* everything from it until the next line is a comment.
*
* @param string $line The history line to parse
*
* @return string | null
*/
protected function parseHistoryLine($line)
{
// empty line, comment or timestamp
if (!$line || $line[0] === "\0") {
return;
}
// if "\0" is found in an entry, then
// everything from it until the end of line is a comment.
if (($pos = strpos($line, "\0")) !== false) {
$line = substr($line, 0, $pos);
}
return ($line !== '') ? Str::unvis($line) : null;
}
}

View file

@ -0,0 +1,76 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Readline;
/**
* An interface abstracting the various readline_* functions.
*/
interface Readline
{
/**
* Check whether this Readline class is supported by the current system.
*
* @return bool
*/
public static function isSupported();
/**
* Add a line to the command history.
*
* @param string $line
*
* @return bool Success
*/
public function addHistory($line);
/**
* Clear the command history.
*
* @return bool Success
*/
public function clearHistory();
/**
* List the command history.
*
* @return array
*/
public function listHistory();
/**
* Read the command history.
*
* @return bool Success
*/
public function readHistory();
/**
* Read a single line of input from the user.
*
* @param null|string $prompt
*
* @return false|string
*/
public function readline($prompt = null);
/**
* Redraw readline to redraw the display.
*/
public function redisplay();
/**
* Write the command history to a file.
*
* @return bool Success
*/
public function writeHistory();
}

View file

@ -0,0 +1,146 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Readline;
use Psy\Exception\BreakException;
/**
* An array-based Readline emulation implementation.
*/
class Transient implements Readline
{
private $history;
private $historySize;
private $eraseDups;
/**
* Transient Readline is always supported.
*
* {@inheritdoc}
*/
public static function isSupported()
{
return true;
}
/**
* Transient Readline constructor.
*/
public function __construct($historyFile = null, $historySize = 0, $eraseDups = false)
{
// don't do anything with the history file...
$this->history = array();
$this->historySize = $historySize;
$this->eraseDups = $eraseDups;
}
/**
* {@inheritdoc}
*/
public function addHistory($line)
{
if ($this->eraseDups) {
if (($key = array_search($line, $this->history)) !== false) {
unset($this->history[$key]);
}
}
$this->history[] = $line;
if ($this->historySize > 0) {
$histsize = count($this->history);
if ($histsize > $this->historySize) {
$this->history = array_slice($this->history, $histsize - $this->historySize);
}
}
$this->history = array_values($this->history);
return true;
}
/**
* {@inheritdoc}
*/
public function clearHistory()
{
$this->history = array();
return true;
}
/**
* {@inheritdoc}
*/
public function listHistory()
{
return $this->history;
}
/**
* {@inheritdoc}
*/
public function readHistory()
{
return true;
}
/**
* {@inheritdoc}
*
* @throws BreakException if user hits Ctrl+D
*
* @return string
*/
public function readline($prompt = null)
{
echo $prompt;
return rtrim(fgets($this->getStdin(), 1024));
}
/**
* {@inheritdoc}
*/
public function redisplay()
{
// noop
}
/**
* {@inheritdoc}
*/
public function writeHistory()
{
return true;
}
/**
* Get a STDIN file handle.
*
* @throws BreakException if user hits Ctrl+D
*
* @return resource
*/
private function getStdin()
{
if (!isset($this->stdin)) {
$this->stdin = fopen('php://stdin', 'r');
}
if (feof($this->stdin)) {
throw new BreakException('Ctrl+D');
}
return $this->stdin;
}
}

View file

@ -0,0 +1,151 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Reflection;
/**
* Somehow the standard reflection library doesn't include constants.
*
* ReflectionConstant corrects that omission.
*/
class ReflectionConstant implements \Reflector
{
private $class;
private $name;
private $value;
/**
* Construct a ReflectionConstant object.
*
* @param mixed $class
* @param string $name
*/
public function __construct($class, $name)
{
if (!$class instanceof \ReflectionClass) {
$class = new \ReflectionClass($class);
}
$this->class = $class;
$this->name = $name;
$constants = $class->getConstants();
if (!array_key_exists($name, $constants)) {
throw new \InvalidArgumentException('Unknown constant: ' . $name);
}
$this->value = $constants[$name];
}
/**
* Gets the declaring class.
*
* @return string
*/
public function getDeclaringClass()
{
$parent = $this->class;
// Since we don't have real reflection constants, we can't see where
// it's actually defined. Let's check for a constant that is also
// available on the parent class which has exactly the same value.
//
// While this isn't _technically_ correct, it's prolly close enough.
do {
$class = $parent;
$parent = $class->getParentClass();
} while ($parent && $parent->hasConstant($this->name) && $parent->getConstant($this->name) === $this->value);
return $class;
}
/**
* Gets the constant name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Gets the value of the constant.
*
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* Gets the constant's file name.
*
* Currently returns null, because if it returns a file name the signature
* formatter will barf.
*/
public function getFileName()
{
return;
// return $this->class->getFileName();
}
/**
* Get the code start line.
*
* @throws \RuntimeException
*/
public function getStartLine()
{
throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)');
}
/**
* Get the code end line.
*
* @throws \RuntimeException
*/
public function getEndLine()
{
return $this->getStartLine();
}
/**
* Get the constant's docblock.
*
* @return false
*/
public function getDocComment()
{
return false;
}
/**
* Export the constant? I don't think this is possible.
*
* @throws \RuntimeException
*/
public static function export()
{
throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)');
}
/**
* To string.
*
* @return string
*/
public function __toString()
{
return $this->getName();
}
}

View file

@ -0,0 +1,152 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Reflection;
/**
* A fake ReflectionFunction but for language constructs.
*/
class ReflectionLanguageConstruct extends \ReflectionFunctionAbstract
{
public $keyword;
/**
* Language construct parameter definitions.
*/
private static $languageConstructs = array(
'isset' => array(
'var' => array(),
'...' => array(
'isOptional' => true,
'defaultValue' => null,
),
),
'unset' => array(
'var' => array(),
'...' => array(
'isOptional' => true,
'defaultValue' => null,
),
),
'empty' => array(
'var' => array(),
),
'echo' => array(
'arg1' => array(),
'...' => array(
'isOptional' => true,
'defaultValue' => null,
),
),
'print' => array(
'arg' => array(),
),
'die' => array(
'status' => array(
'isOptional' => true,
'defaultValue' => 0,
),
),
'exit' => array(
'status' => array(
'isOptional' => true,
'defaultValue' => 0,
),
),
);
/**
* Construct a ReflectionLanguageConstruct object.
*
* @param string $name
*/
public function __construct($keyword)
{
if (self::isLanguageConstruct($keyword)) {
throw new \InvalidArgumentException('Unknown language construct: ' . $keyword);
}
$this->keyword = $keyword;
}
/**
* This can't (and shouldn't) do anything :).
*
* @throws \RuntimeException
*/
public static function export($name)
{
throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)');
}
/**
* Get language construct name.
*
* @return string
*/
public function getName()
{
return $this->keyword;
}
/**
* None of these return references.
*
* @return bool
*/
public function returnsReference()
{
return false;
}
/**
* Get language construct params.
*
* @return
*/
public function getParameters()
{
$params = array();
foreach (self::$languageConstructs[$this->keyword] as $parameter => $opts) {
array_push($params, new ReflectionLanguageConstructParameter($this->keyword, $parameter, $opts));
}
return $params;
}
/**
* To string.
*
* @return string
*/
public function __toString()
{
return $this->getName();
}
/**
* Check whether keyword is a (known) language construct.
*
* @param $keyword
*
* @return bool
*/
public static function isLanguageConstruct($keyword)
{
return array_key_exists($keyword, self::$languageConstructs);
}
}

View file

@ -0,0 +1,103 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Reflection;
/**
* A fake ReflectionParameter but for language construct parameters.
*
* It stubs out all the important bits and returns whatever was passed in $opts.
*/
class ReflectionLanguageConstructParameter extends \ReflectionParameter
{
private $function;
private $parameter;
private $opts;
public function __construct($function, $parameter, array $opts)
{
$this->function = $function;
$this->parameter = $parameter;
$this->opts = $opts;
}
/**
* No class here.
*/
public function getClass()
{
return;
}
/**
* Is the param an array?
*
* @return bool
*/
public function isArray()
{
return array_key_exists('isArray', $this->opts) && $this->opts['isArray'];
}
/**
* Get param default value.
*
* @return mixed
*/
public function getDefaultValue()
{
if ($this->isDefaultValueAvailable()) {
return $this->opts['defaultValue'];
}
}
/**
* Get param name.
*
* @return string
*/
public function getName()
{
return $this->parameter;
}
/**
* Is the param optional?
*
* @return bool
*/
public function isOptional()
{
return array_key_exists('isOptional', $this->opts) && $this->opts['isOptional'];
}
/**
* Does the param have a default value?
*
* @return bool
*/
public function isDefaultValueAvailable()
{
return array_key_exists('defaultValue', $this->opts);
}
/**
* Is the param passed by reference?
*
* (I don't think this is true for anything we need to fake a param for)
*
* @return bool
*/
public function isPassedByReference()
{
return array_key_exists('isPassedByReference', $this->opts) && $this->opts['isPassedByReference'];
}
}

1033
vendor/psy/psysh/src/Psy/Shell.php vendored Normal file

File diff suppressed because it is too large Load diff

150
vendor/psy/psysh/src/Psy/Sudo.php vendored Normal file
View file

@ -0,0 +1,150 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy;
/**
* Helpers for bypassing visibility restrictions, mostly used in code generated
* by the `sudo` command.
*/
class Sudo
{
/**
* Fetch a property of an object, bypassing visibility restrictions.
*
* @param object $object
* @param string $property property name
*
* @return mixed Value of $object->property
*/
public static function fetchProperty($object, $property)
{
$refl = new \ReflectionObject($object);
$prop = $refl->getProperty($property);
$prop->setAccessible(true);
return $prop->getValue($object);
}
/**
* Assign the value of a property of an object, bypassing visibility restrictions.
*
* @param object $object
* @param string $property property name
* @param mixed $value
*
* @return mixed Value of $object->property
*/
public static function assignProperty($object, $property, $value)
{
$refl = new \ReflectionObject($object);
$prop = $refl->getProperty($property);
$prop->setAccessible(true);
$prop->setValue($object, $value);
return $value;
}
/**
* Call a method on an object, bypassing visibility restrictions.
*
* @param object $object
* @param string $method method name
* @param mixed $args...
*
* @return mixed
*/
public static function callMethod($object, $method, $args = null)
{
$args = func_get_args();
$object = array_shift($args);
$method = array_shift($args);
$refl = new \ReflectionObject($object);
$reflMethod = $refl->getMethod($method);
$reflMethod->setAccessible(true);
return $reflMethod->invokeArgs($object, $args);
}
/**
* Fetch a property of a class, bypassing visibility restrictions.
*
* @param string|object $class class name or instance
* @param string $property property name
*
* @return mixed Value of $class::$property
*/
public static function fetchStaticProperty($class, $property)
{
$refl = new \ReflectionClass($class);
$prop = $refl->getProperty($property);
$prop->setAccessible(true);
return $prop->getValue();
}
/**
* Assign the value of a static property of a class, bypassing visibility restrictions.
*
* @param string|object $class class name or instance
* @param string $property property name
* @param mixed $value
*
* @return mixed Value of $class::$property
*/
public static function assignStaticProperty($class, $property, $value)
{
$refl = new \ReflectionClass($class);
$prop = $refl->getProperty($property);
$prop->setAccessible(true);
$prop->setValue($value);
return $value;
}
/**
* Call a static method on a class, bypassing visibility restrictions.
*
* @param string|object $class class name or instance
* @param string $method method name
* @param mixed $args...
*
* @return mixed
*/
public static function callStatic($class, $method, $args = null)
{
$args = func_get_args();
$class = array_shift($args);
$method = array_shift($args);
$refl = new \ReflectionClass($class);
$reflMethod = $refl->getMethod($method);
$reflMethod->setAccessible(true);
return $reflMethod->invokeArgs(null, $args);
}
/**
* Fetch a class constant, bypassing visibility restrictions.
*
* @param string|object $class class name or instance
* @param string $const constant name
*
* @return mixed
*/
public static function fetchClassConst($class, $const)
{
$refl = new \ReflectionClass($class);
return $refl->getConstant($const);
}
}

View file

@ -0,0 +1,115 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Sudo;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\StaticPropertyFetch;
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
use PhpParser\Node\Scalar\String_;
use PhpParser\NodeVisitorAbstract;
/**
* A PHP Parser node visitor which rewrites property and method access to use
* the Psy\Sudo visibility bypass methods.
*
* @todo handle assigning by reference
*/
class SudoVisitor extends NodeVisitorAbstract
{
const SUDO_CLASS = 'Psy\Sudo';
const PROPERTY_FETCH = 'fetchProperty';
const PROPERTY_ASSIGN = 'assignProperty';
const METHOD_CALL = 'callMethod';
const STATIC_PROPERTY_FETCH = 'fetchStaticProperty';
const STATIC_PROPERTY_ASSIGN = 'assignStaticProperty';
const STATIC_CALL = 'callStatic';
const CLASS_CONST_FETCH = 'fetchClassConst';
/**
* {@inheritdoc}
*/
public function enterNode(Node $node)
{
if ($node instanceof PropertyFetch) {
$args = array(
$node->var,
is_string($node->name) ? new String_($node->name) : $node->name,
);
return $this->prepareCall(self::PROPERTY_FETCH, $args);
} elseif ($node instanceof Assign && $node->var instanceof PropertyFetch) {
$target = $node->var;
$args = array(
$target->var,
is_string($target->name) ? new String_($target->name) : $target->name,
$node->expr,
);
return $this->prepareCall(self::PROPERTY_ASSIGN, $args);
} elseif ($node instanceof MethodCall) {
$args = $node->args;
array_unshift($args, new Arg(is_string($node->name) ? new String_($node->name) : $node->name));
array_unshift($args, new Arg($node->var));
// not using prepareCall because the $node->args we started with are already Arg instances
return new StaticCall(new FullyQualifiedName(self::SUDO_CLASS), self::METHOD_CALL, $args);
} elseif ($node instanceof StaticPropertyFetch) {
$class = $node->class instanceof Name ? (string) $node->class : $node->class;
$args = array(
is_string($class) ? new String_($class) : $class,
is_string($node->name) ? new String_($node->name) : $node->name,
);
return $this->prepareCall(self::STATIC_PROPERTY_FETCH, $args);
} elseif ($node instanceof Assign && $node->var instanceof StaticPropertyFetch) {
$target = $node->var;
$class = $target->class instanceof Name ? (string) $target->class : $target->class;
$args = array(
is_string($class) ? new String_($class) : $class,
is_string($target->name) ? new String_($target->name) : $target->name,
$node->expr,
);
return $this->prepareCall(self::STATIC_PROPERTY_ASSIGN, $args);
} elseif ($node instanceof StaticCall) {
$args = $node->args;
$class = $node->class instanceof Name ? (string) $node->class : $node->class;
array_unshift($args, new Arg(is_string($node->name) ? new String_($node->name) : $node->name));
array_unshift($args, new Arg(is_string($class) ? new String_($class) : $class));
// not using prepareCall because the $node->args we started with are already Arg instances
return new StaticCall(new FullyQualifiedName(self::SUDO_CLASS), self::STATIC_CALL, $args);
} elseif ($node instanceof ClassConstFetch) {
$class = $node->class instanceof Name ? (string) $node->class : $node->class;
$args = array(
is_string($class) ? new String_($class) : $class,
is_string($node->name) ? new String_($node->name) : $node->name,
);
return $this->prepareCall(self::CLASS_CONST_FETCH, $args);
}
}
private function prepareCall($method, $args)
{
return new StaticCall(new FullyQualifiedName(self::SUDO_CLASS), $method, array_map(function ($arg) {
return new Arg($arg);
}, $args));
}
}

View file

@ -0,0 +1,112 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\TabCompletion;
use Psy\TabCompletion\Matcher\AbstractMatcher;
/**
* A readline tab completion service.
*
* @author Marc Garcia <markcial@gmail.com>
*/
class AutoCompleter
{
/** @var Matcher\AbstractMatcher[] */
protected $matchers;
/**
* Register a tab completion Matcher.
*
* @param AbstractMatcher $matcher
*/
public function addMatcher(AbstractMatcher $matcher)
{
$this->matchers[] = $matcher;
}
/**
* Activate readline tab completion.
*/
public function activate()
{
readline_completion_function(array(&$this, 'callback'));
}
/**
* Handle readline completion.
*
* @param string $input Readline current word
* @param int $index Current word index
* @param array $info readline_info() data
*
* @return array
*/
public function processCallback($input, $index, $info = array())
{
// Some (Windows?) systems provide incomplete `readline_info`, so let's
// try to work around it.
$line = $info['line_buffer'];
if (isset($info['end'])) {
$line = substr($line, 0, $info['end']);
}
if ($line === '' && $input !== '') {
$line = $input;
}
$tokens = token_get_all('<?php ' . $line);
// remove whitespaces
$tokens = array_filter($tokens, function ($token) {
return !AbstractMatcher::tokenIs($token, AbstractMatcher::T_WHITESPACE);
});
$matches = array();
foreach ($this->matchers as $matcher) {
if ($matcher->hasMatched($tokens)) {
$matches = array_merge($matcher->getMatches($tokens), $matches);
}
}
$matches = array_unique($matches);
return !empty($matches) ? $matches : array('');
}
/**
* The readline_completion_function callback handler.
*
* @see processCallback
*
* @param $input
* @param $index
*
* @return array
*/
public function callback($input, $index)
{
return $this->processCallback($input, $index, readline_info());
}
/**
* Remove readline callback handler on destruct.
*/
public function __destruct()
{
// PHP didn't implement the whole readline API when they first switched
// to libedit. And they still haven't.
//
// So this is a thing to make PsySH work on 5.3.x:
if (function_exists('readline_callback_handler_remove')) {
readline_callback_handler_remove();
}
}
}

Some files were not shown because too many files have changed in this diff Show more