Skip to content

Commit

Permalink
Merge branch 'release/0.9.6'
Browse files Browse the repository at this point in the history
  • Loading branch information
bobthecow committed Jun 10, 2018
2 parents 0951e91 + 6d4e905 commit 4a2ce86
Show file tree
Hide file tree
Showing 18 changed files with 597 additions and 24 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ PsySH is a runtime developer console, interactive debugger and [REPL](https://en

### [💾 Installation](https://github.com/bobthecow/psysh/wiki/Installation)
* [📕 PHP manual installation](https://github.com/bobthecow/psysh/wiki/PHP-manual)
* <a class="internal present" href="https://github.com/bobthecow/psysh/wiki/Windows"><img src="https://user-images.githubusercontent.com/53660/40878809-407e8368-664b-11e8-8455-f11602c41dfe.png" width="18"> Windows</a>

### [🖥 Usage](https://github.com/bobthecow/psysh/wiki/Usage)
* [✨ Magic variables](https://github.com/bobthecow/psysh/wiki/Magic-variables)
Expand Down
6 changes: 2 additions & 4 deletions src/CodeCleaner/ImplicitReturnPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
use PhpParser\Node;
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\Expression;
Expand Down Expand Up @@ -48,7 +46,7 @@ private function addImplicitReturn(array $nodes)
{
// If nodes is empty, it can't have a return value.
if (empty($nodes)) {
return [new Return_(new New_(new FullyQualifiedName('Psy\CodeCleaner\NoReturnValue')))];
return [new Return_(NoReturnValue::create())];
}

$last = end($nodes);
Expand Down Expand Up @@ -104,7 +102,7 @@ private function addImplicitReturn(array $nodes)
// because code outside namespace statements doesn't really work, and
// there's already an implicit return in the namespace statement anyway.
if (self::isNonExpressionStmt($last)) {
$nodes[] = new Return_(new New_(new FullyQualifiedName('Psy\CodeCleaner\NoReturnValue')));
$nodes[] = new Return_(NoReturnValue::create());
}

return $nodes;
Expand Down
9 changes: 8 additions & 1 deletion src/CodeCleaner/ListPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,14 @@ public function enterNode(Node $node)
throw new ParseErrorException('Cannot use empty list', $node->var->getLine());
}

$itemFound = false;
foreach ($items as $item) {
if ($item === null) {
throw new ParseErrorException('Cannot use empty list', $item->getLine());
continue;
}

$itemFound = true;

// List_->$vars in PHP-Parser 2.x is Variable instead of ArrayItem.
if (!$this->atLeastPhp71 && $item instanceof ArrayItem && $item->key !== null) {
$msg = 'Syntax error, unexpected T_CONSTANT_ENCAPSED_STRING, expecting \',\' or \')\'';
Expand All @@ -78,5 +81,9 @@ public function enterNode(Node $node)
throw new ParseErrorException($msg, $item->getLine());
}
}

if (!$itemFound) {
throw new ParseErrorException('Cannot use empty list');
}
}
}
13 changes: 12 additions & 1 deletion src/CodeCleaner/NoReturnValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

namespace Psy\CodeCleaner;

use PhpParser\Node\Expr\New_;
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;

/**
* A class used internally by CodeCleaner to represent input, such as
* non-expression statements, with no return value.
Expand All @@ -20,5 +23,13 @@
*/
class NoReturnValue
{
// this space intentionally left blank
/**
* Get PhpParser AST expression for creating a new NoReturnValue.
*
* @return PhpParser\Node\Expr\New_
*/
public static function create()
{
return new New_(new FullyQualifiedName('Psy\CodeCleaner\NoReturnValue'));
}
}
22 changes: 18 additions & 4 deletions src/Command/ThrowUpCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
namespace Psy\Command;

use PhpParser\Node\Arg;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Throw_;
use PhpParser\PrettyPrinter\Standard as Printer;
use Psy\Context;
Expand Down Expand Up @@ -85,6 +87,7 @@ protected function configure()
<return>>>> throw-up</return>
<return>>>> throw-up $e</return>
<return>>>> throw-up new Exception('WHEEEEEE!')</return>
<return>>>> throw-up "bye!"</return>
HELP
);
}
Expand Down Expand Up @@ -126,13 +129,24 @@ private function prepareArgs($code = null)
$code = '<?php ' . $code;
}

$expr = $this->parse($code);

if (count($expr) !== 1) {
$nodes = $this->parse($code);
if (count($nodes) !== 1) {
throw new \InvalidArgumentException('No idea how to throw this');
}

return [new Arg($expr[0])];
$node = $nodes[0];

// Make this work for PHP Parser v3.x
$expr = isset($node->expr) ? $node->expr : $node;

$args = [new Arg($expr, false, false, $node->getAttributes())];

// Allow throwing via a string, e.g. `throw-up "SUP"`
if ($expr instanceof String_) {
return [new New_(new FullyQualifiedName('Exception'), $args)];
}

return $args;
}

/**
Expand Down
125 changes: 121 additions & 4 deletions src/Command/TimeitCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@

namespace Psy\Command;

use PhpParser\NodeTraverser;
use PhpParser\PrettyPrinter\Standard as Printer;
use Psy\Command\TimeitCommand\TimeitVisitor;
use Psy\Input\CodeArgument;
use Psy\ParserFactory;
use Psy\Shell;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
Expand All @@ -25,6 +29,29 @@ class TimeitCommand extends Command
const RESULT_MSG = '<info>Command took %.6f seconds to complete.</info>';
const AVG_RESULT_MSG = '<info>Command took %.6f seconds on average (%.6f median; %.6f total) to complete.</info>';

private static $start = null;
private static $times = [];

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 TimeitVisitor());

$this->printer = new Printer();

parent::__construct($name);
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -57,15 +84,20 @@ protected function execute(InputInterface $input, OutputInterface $output)
$num = $input->getOption('num') ?: 1;
$shell = $this->getApplication();

$times = [];
$instrumentedCode = $this->instrumentCode($code);

self::$times = [];

for ($i = 0; $i < $num; $i++) {
$start = microtime(true);
$_ = $shell->execute($code);
$times[] = microtime(true) - $start;
$_ = $shell->execute($instrumentedCode);
$this->ensureEndMarked();
}

$shell->writeReturnValue($_);

$times = self::$times;
self::$times = [];

if ($num === 1) {
$output->writeln(sprintf(self::RESULT_MSG, $times[0]));
} else {
Expand All @@ -76,4 +108,89 @@ protected function execute(InputInterface $input, OutputInterface $output)
$output->writeln(sprintf(self::AVG_RESULT_MSG, $total / $num, $median, $total));
}
}

/**
* Internal method for marking the start of timeit execution.
*
* A static call to this method will be injected at the start of the timeit
* input code to instrument the call. We will use the saved start time to
* more accurately calculate time elapsed during execution.
*/
public static function markStart()
{
self::$start = microtime(true);
}

/**
* Internal method for marking the end of timeit execution.
*
* A static call to this method is injected by TimeitVisitor at the end
* of the timeit input code to instrument the call.
*
* Note that this accepts an optional $ret parameter, which is used to pass
* the return value of the last statement back out of timeit. This saves us
* a bunch of code rewriting shenanigans.
*
* @param mixed $ret
*
* @return mixed it just passes $ret right back
*/
public static function markEnd($ret = null)
{
self::$times[] = microtime(true) - self::$start;
self::$start = null;

return $ret;
}

/**
* Ensure that the end of code execution was marked.
*
* The end *should* be marked in the instrumented code, but just in case
* we'll add a fallback here.
*/
private function ensureEndMarked()
{
if (self::$start !== null) {
self::markEnd();
}
}

/**
* Instrument code for timeit execution.
*
* This inserts `markStart` and `markEnd` calls to ensure that (reasonably)
* accurate times are recorded for just the code being executed.
*
* @param string $code
*
* @return string
*/
private function instrumentCode($code)
{
return $this->printer->prettyPrint($this->traverser->traverse($this->parse($code)));
}

/**
* Lex and parse a string of code into statements.
*
* @param string $code
*
* @return array Statements
*/
private function parse($code)
{
$code = '<?php ' . $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 . ';');
}
}
}
Loading

0 comments on commit 4a2ce86

Please sign in to comment.