From d6844e569ed490528b38be2324e641fb2e559f8b Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sat, 2 Jun 2018 11:54:26 -0700 Subject: [PATCH 01/24] Add Windows link to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 45dd6a3b2..710434caf 100644 --- a/README.md +++ b/README.md @@ -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) + * Windows ### [🖥 Usage](https://github.com/bobthecow/psysh/wiki/Usage) * [✨ Magic variables](https://github.com/bobthecow/psysh/wiki/Magic-variables) From ad29bcc5676492b70264f5c41955abe449e19067 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 3 Jun 2018 15:21:41 +0000 Subject: [PATCH 02/24] Add test coverage for throw-up command. Fix a bug when invoking throw-up command with arguments that the tests exposed :-/ --- src/Command/ThrowUpCommand.php | 10 +++--- test/Command/ThrowUpCommandTest.php | 54 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 test/Command/ThrowUpCommandTest.php diff --git a/src/Command/ThrowUpCommand.php b/src/Command/ThrowUpCommand.php index 4b7003d10..1154d7aaa 100644 --- a/src/Command/ThrowUpCommand.php +++ b/src/Command/ThrowUpCommand.php @@ -126,13 +126,15 @@ private function prepareArgs($code = null) $code = '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]; + $args = [new Arg($node->expr, false, false, $node->getAttributes())]; + + return $args; } /** diff --git a/test/Command/ThrowUpCommandTest.php b/test/Command/ThrowUpCommandTest.php new file mode 100644 index 000000000..287c9057b --- /dev/null +++ b/test/Command/ThrowUpCommandTest.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Command; + +use Symfony\Component\Console\Tester\CommandTester; +use Psy\Command\ThrowUpCommand; +use Psy\Shell; + +class ThrowUpCommandTest extends \PHPUnit\Framework\TestCase +{ + /** + * @dataProvider executeThis + */ + public function testExecute($args, $hasCode, $expect) + { + $shell = $this->getMockBuilder('Psy\\Shell') + ->setMethods(['hasCode', 'addCode']) + ->getMock(); + + $shell->expects($this->once())->method('hasCode')->willReturn($hasCode); + $shell->expects($this->once()) + ->method('addCode') + ->with($this->equalTo($expect), $this->equalTo(!$hasCode)); + + $command = new ThrowUpCommand(); + $command->setApplication($shell); + $tester = new CommandTester($command); + $tester->execute($args); + $this->assertEquals('', $tester->getDisplay()); + } + + public function executeThis() + { + $throw = 'throw \Psy\Exception\ThrowUpException::fromThrowable'; + + return [ + [[], false, $throw . '($_e);'], + [[], true, $throw . '($_e);'], + + [['exception' => '$ex'], true, $throw . '($ex);'], + [['exception' => 'getException()'], true, $throw . '(getException());'], + [['exception' => 'new \\Exception("WAT")'], true, $throw . '(new \\Exception("WAT"));'], + ]; + } +} From 9fed4843a8ad324e411b96ab24faf9643eb4a85d Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 3 Jun 2018 08:24:07 -0700 Subject: [PATCH 03/24] Add support for throwing strings via throw-up command. --- src/Command/ThrowUpCommand.php | 7 +++++++ test/Command/ThrowUpCommandTest.php | 3 +++ 2 files changed, 10 insertions(+) diff --git a/src/Command/ThrowUpCommand.php b/src/Command/ThrowUpCommand.php index 1154d7aaa..5c1d3ed04 100644 --- a/src/Command/ThrowUpCommand.php +++ b/src/Command/ThrowUpCommand.php @@ -16,6 +16,8 @@ use PhpParser\Node\Expr\Variable; use PhpParser\Node\Name\FullyQualified as FullyQualifiedName; use PhpParser\Node\Stmt\Throw_; +use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Expr\New_; use PhpParser\PrettyPrinter\Standard as Printer; use Psy\Context; use Psy\ContextAware; @@ -134,6 +136,11 @@ private function prepareArgs($code = null) $node = $nodes[0]; $args = [new Arg($node->expr, false, false, $node->getAttributes())]; + // Allow throwing via a string, e.g. `throw-up "SUP"` + if ($node->expr instanceof String_) { + return [new New_(new FullyQualifiedName('Exception'), $args)]; + } + return $args; } diff --git a/test/Command/ThrowUpCommandTest.php b/test/Command/ThrowUpCommandTest.php index 287c9057b..bf3bfeedf 100644 --- a/test/Command/ThrowUpCommandTest.php +++ b/test/Command/ThrowUpCommandTest.php @@ -49,6 +49,9 @@ public function executeThis() [['exception' => '$ex'], true, $throw . '($ex);'], [['exception' => 'getException()'], true, $throw . '(getException());'], [['exception' => 'new \\Exception("WAT")'], true, $throw . '(new \\Exception("WAT"));'], + + [['exception' => '\'some string\''], true, $throw . '(new \\Exception(\'some string\'));'], + [['exception' => '"WHEEEEEEE!"'], true, $throw . '(new \\Exception("WHEEEEEEE!"));'], ]; } } From 64efdc781849536ce45c5ca4e9a65164890218ba Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 3 Jun 2018 08:25:49 -0700 Subject: [PATCH 04/24] Add throw-up with string example to help. --- src/Command/ThrowUpCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Command/ThrowUpCommand.php b/src/Command/ThrowUpCommand.php index 5c1d3ed04..e784acfea 100644 --- a/src/Command/ThrowUpCommand.php +++ b/src/Command/ThrowUpCommand.php @@ -87,6 +87,7 @@ protected function configure() >>> throw-up >>> throw-up $e >>> throw-up new Exception('WHEEEEEE!') +>>> throw-up "bye!" HELP ); } From 59755156c464b4456baa76e3eeb51d268abaf33c Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 3 Jun 2018 08:27:49 -0700 Subject: [PATCH 05/24] Fix CS :) --- src/Command/ThrowUpCommand.php | 4 ++-- test/Command/ThrowUpCommandTest.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Command/ThrowUpCommand.php b/src/Command/ThrowUpCommand.php index e784acfea..ac5b4060e 100644 --- a/src/Command/ThrowUpCommand.php +++ b/src/Command/ThrowUpCommand.php @@ -12,12 +12,12 @@ 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\Stmt\Throw_; use PhpParser\Node\Scalar\String_; -use PhpParser\Node\Expr\New_; +use PhpParser\Node\Stmt\Throw_; use PhpParser\PrettyPrinter\Standard as Printer; use Psy\Context; use Psy\ContextAware; diff --git a/test/Command/ThrowUpCommandTest.php b/test/Command/ThrowUpCommandTest.php index bf3bfeedf..5006986d2 100644 --- a/test/Command/ThrowUpCommandTest.php +++ b/test/Command/ThrowUpCommandTest.php @@ -1,9 +1,9 @@ + * (c) 2012-2018 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,9 +11,9 @@ namespace Symfony\Component\Console\Tests\Command; -use Symfony\Component\Console\Tester\CommandTester; use Psy\Command\ThrowUpCommand; use Psy\Shell; +use Symfony\Component\Console\Tester\CommandTester; class ThrowUpCommandTest extends \PHPUnit\Framework\TestCase { From 8b5a9aaf6398bd4055617655aa9dd0623bb1d743 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 3 Jun 2018 09:46:27 -0700 Subject: [PATCH 06/24] Moar test coverage for throw-up. --- test/Command/ThrowUpCommandTest.php | 48 ++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/test/Command/ThrowUpCommandTest.php b/test/Command/ThrowUpCommandTest.php index 5006986d2..511c5809c 100644 --- a/test/Command/ThrowUpCommandTest.php +++ b/test/Command/ThrowUpCommandTest.php @@ -20,7 +20,7 @@ class ThrowUpCommandTest extends \PHPUnit\Framework\TestCase /** * @dataProvider executeThis */ - public function testExecute($args, $hasCode, $expect) + public function testExecute($args, $hasCode, $expect, $addSilent = true) { $shell = $this->getMockBuilder('Psy\\Shell') ->setMethods(['hasCode', 'addCode']) @@ -29,7 +29,7 @@ public function testExecute($args, $hasCode, $expect) $shell->expects($this->once())->method('hasCode')->willReturn($hasCode); $shell->expects($this->once()) ->method('addCode') - ->with($this->equalTo($expect), $this->equalTo(!$hasCode)); + ->with($this->equalTo($expect), $this->equalTo($addSilent)); $command = new ThrowUpCommand(); $command->setApplication($shell); @@ -44,14 +44,46 @@ public function executeThis() return [ [[], false, $throw . '($_e);'], - [[], true, $throw . '($_e);'], - [['exception' => '$ex'], true, $throw . '($ex);'], - [['exception' => 'getException()'], true, $throw . '(getException());'], - [['exception' => 'new \\Exception("WAT")'], true, $throw . '(new \\Exception("WAT"));'], + [['exception' => '$ex'], false, $throw . '($ex);'], + [['exception' => 'getException()'], false, $throw . '(getException());'], + [['exception' => 'new \\Exception("WAT")'], false, $throw . '(new \\Exception("WAT"));'], - [['exception' => '\'some string\''], true, $throw . '(new \\Exception(\'some string\'));'], - [['exception' => '"WHEEEEEEE!"'], true, $throw . '(new \\Exception("WHEEEEEEE!"));'], + [['exception' => '\'some string\''], false, $throw . '(new \\Exception(\'some string\'));'], + [['exception' => '"WHEEEEEEE!"'], false, $throw . '(new \\Exception("WHEEEEEEE!"));'], + + // Everything should work with or without semicolons. + [['exception' => '$ex;'], false, $throw . '($ex);'], + [['exception' => '"WHEEEEEEE!";'], false, $throw . '(new \\Exception("WHEEEEEEE!"));'], + + // Don't add as silent code if we've already got code. + [[], true, $throw . '($_e);', false], + [['exception' => 'getException()'], true, $throw . '(getException());', false], + [['exception' => '\'some string\''], true, $throw . '(new \\Exception(\'some string\'));', false], ]; } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage No idea how to throw this + */ + public function testMultipleArgsThrowsException() + { + $command = new ThrowUpCommand(); + $command->setApplication(new Shell()); + $tester = new CommandTester($command); + $tester->execute(['exception' => 'foo(); bar()']); + } + + /** + * @expectedException \PhpParser\Error + * @expectedExceptionMessage Syntax error, unexpected ')' on line 1 + */ + public function testParseErrorThrowsException() + { + $command = new ThrowUpCommand(); + $command->setApplication(new Shell()); + $tester = new CommandTester($command); + $tester->execute(['exception' => 'foo)']); + } } From 292a1796ad9a73748c3868b224d626d5d91f8a29 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 3 Jun 2018 09:46:43 -0700 Subject: [PATCH 07/24] Fix memory leak with parser test cases. --- test/CodeCleaner/CodeCleanerTestCase.php | 6 ++++++ test/ParserTestCase.php | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/test/CodeCleaner/CodeCleanerTestCase.php b/test/CodeCleaner/CodeCleanerTestCase.php index 996dbc7c0..6962c1141 100644 --- a/test/CodeCleaner/CodeCleanerTestCase.php +++ b/test/CodeCleaner/CodeCleanerTestCase.php @@ -19,6 +19,12 @@ class CodeCleanerTestCase extends ParserTestCase { protected $pass; + public function tearDown() + { + $this->pass = null; + parent::tearDown(); + } + protected function setPass(CodeCleanerPass $pass) { $this->pass = $pass; diff --git a/test/ParserTestCase.php b/test/ParserTestCase.php index f3627a2a4..1b39b5f15 100644 --- a/test/ParserTestCase.php +++ b/test/ParserTestCase.php @@ -21,6 +21,13 @@ class ParserTestCase extends \PHPUnit\Framework\TestCase private $parser; private $printer; + public function tearDown() + { + $this->traverser = null; + $this->parser = null; + $this->printer = null; + } + protected function parse($code, $prefix = ' Date: Sun, 3 Jun 2018 09:57:33 -0700 Subject: [PATCH 08/24] Fix throw-up with PHP Parser < 4.x --- src/Command/ThrowUpCommand.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Command/ThrowUpCommand.php b/src/Command/ThrowUpCommand.php index ac5b4060e..99af9a8aa 100644 --- a/src/Command/ThrowUpCommand.php +++ b/src/Command/ThrowUpCommand.php @@ -135,10 +135,14 @@ private function prepareArgs($code = null) } $node = $nodes[0]; - $args = [new Arg($node->expr, false, false, $node->getAttributes())]; + + // 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 ($node->expr instanceof String_) { + if ($expr instanceof String_) { return [new New_(new FullyQualifiedName('Exception'), $args)]; } From ed73cabc3938535d85d24459f9bac366286ed6b5 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 3 Jun 2018 13:12:53 -0700 Subject: [PATCH 09/24] Ignore coverage for the copypasta in ShellInput. --- src/Input/ShellInput.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Input/ShellInput.php b/src/Input/ShellInput.php index e0ba28477..34fa2f26c 100644 --- a/src/Input/ShellInput.php +++ b/src/Input/ShellInput.php @@ -105,7 +105,9 @@ private function tokenize($input) ]; } else { // should never happen + // @codeCoverageIgnoreStart throw new \InvalidArgumentException(sprintf('Unable to parse input near "... %s ..."', substr($input, $cursor, 10))); + // @codeCoverageIgnoreEnd } $cursor += strlen($match[0]); @@ -186,6 +188,7 @@ private function parseShellArgument($token, $rest) } // Everything below this is copypasta from ArgvInput private methods + // @codeCoverageIgnoreStart /** * Parses a short option. @@ -323,4 +326,6 @@ private function addLongOption($name, $value) $this->options[$name] = $value; } } + + // @codeCoverageIgnoreEnd } From 64a26056e4fb0d5ce17b3121d4a4f0edc3468d80 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 3 Jun 2018 13:13:02 -0700 Subject: [PATCH 10/24] Add test coverage for exit command. --- test/Command/ExitCommandTest.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/Command/ExitCommandTest.php diff --git a/test/Command/ExitCommandTest.php b/test/Command/ExitCommandTest.php new file mode 100644 index 000000000..45626be2c --- /dev/null +++ b/test/Command/ExitCommandTest.php @@ -0,0 +1,29 @@ +execute([]); + } +} From e3fb4ae46a7c22df1fcf33889e7e0443e676a114 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 3 Jun 2018 13:13:21 -0700 Subject: [PATCH 11/24] Add test coverage for constant signature formatting. --- test/Formatter/SignatureFormatterTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/Formatter/SignatureFormatterTest.php b/test/Formatter/SignatureFormatterTest.php index 28d7ba328..7d4938d0b 100644 --- a/test/Formatter/SignatureFormatterTest.php +++ b/test/Formatter/SignatureFormatterTest.php @@ -13,6 +13,7 @@ use Psy\Formatter\SignatureFormatter; use Psy\Reflection\ReflectionClassConstant; +use Psy\Reflection\ReflectionConstant_; class SignatureFormatterTest extends \PHPUnit\Framework\TestCase { @@ -68,6 +69,18 @@ public function signatureReflectors() new \ReflectionMethod('Psy\Test\Formatter\Fixtures\BoringTrait', 'boringMethod'), 'public function boringMethod($one = 1)', ], + [ + new ReflectionConstant_('E_ERROR'), + 'define("E_ERROR", 1)', + ], + [ + new ReflectionConstant_('PHP_VERSION'), + 'define("PHP_VERSION", "' . PHP_VERSION . '")', + ], + [ + new ReflectionConstant_('__LINE__'), + 'define("__LINE__", null)', // @todo show this as `unknown` in red or something? + ], ]; } From f282beea6fb10231205fc5a51e057f165d9f1764 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 3 Jun 2018 13:17:29 -0700 Subject: [PATCH 12/24] Add coverage for ShellInput throwing for unexpected code argument --- test/Input/ShellInputTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/Input/ShellInputTest.php b/test/Input/ShellInputTest.php index ff6d35744..88f9b1990 100644 --- a/test/Input/ShellInputTest.php +++ b/test/Input/ShellInputTest.php @@ -31,6 +31,21 @@ public function testTokenize($input, $tokens, $message) $this->assertSame($tokens, $p->getValue($input), $message); } + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unexpected CodeArgument before the final position: a + */ + public function testThrowsWhenCodeArgumentNotInFinalPosition() + { + $definition = new InputDefinition([ + new CodeArgument('a', null, CodeArgument::REQUIRED), + new InputArgument('b', null, InputArgument::REQUIRED), + ]); + + $input = new ShellInput('foo bar'); + $input->bind($definition); + } + public function testInputOptionWithGivenString() { $definition = new InputDefinition([ From 64f7043961bac90f7c3329c96cce5bb6953dff8a Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 3 Jun 2018 13:29:58 -0700 Subject: [PATCH 13/24] More shell input test coverage. --- src/Input/ShellInput.php | 5 +++++ test/Input/ShellInputTest.php | 28 +++++++++++++++------------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/Input/ShellInput.php b/src/Input/ShellInput.php index 34fa2f26c..7141d2d4c 100644 --- a/src/Input/ShellInput.php +++ b/src/Input/ShellInput.php @@ -170,6 +170,10 @@ private function parseShellArgument($token, $rest) return; } + // (copypasta) + // + // @codeCoverageIgnoreStart + // 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); @@ -185,6 +189,7 @@ private function parseShellArgument($token, $rest) } throw new \RuntimeException(sprintf('No arguments expected, got "%s".', $token)); + // @codeCoverageIgnoreEnd } // Everything below this is copypasta from ArgvInput private methods diff --git a/test/Input/ShellInputTest.php b/test/Input/ShellInputTest.php index 88f9b1990..88ad5df5b 100644 --- a/test/Input/ShellInputTest.php +++ b/test/Input/ShellInputTest.php @@ -19,18 +19,6 @@ class ShellInputTest extends \PHPUnit\Framework\TestCase { - /** - * @dataProvider getTokenizeData - */ - public function testTokenize($input, $tokens, $message) - { - $input = new ShellInput($input); - $r = new \ReflectionClass('Psy\Input\ShellInput'); - $p = $r->getProperty('tokenPairs'); - $p->setAccessible(true); - $this->assertSame($tokens, $p->getValue($input), $message); - } - /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage Unexpected CodeArgument before the final position: a @@ -63,17 +51,31 @@ public function testInputOptionWithoutCodeArguments() { $definition = new InputDefinition([ new InputOption('foo', null, InputOption::VALUE_REQUIRED), + new InputOption('qux', 'q', InputOption::VALUE_REQUIRED), new InputArgument('bar', null, InputArgument::REQUIRED), new InputArgument('baz', null, InputArgument::REQUIRED), ]); - $input = new ShellInput('--foo=foo bar "baz\\\\n"'); + $input = new ShellInput('--foo=foo -q qux bar "baz\\\\n"'); $input->bind($definition); $this->assertSame('foo', $input->getOption('foo')); + $this->assertSame('qux', $input->getOption('qux')); $this->assertSame('bar', $input->getArgument('bar')); $this->assertSame('baz\\n', $input->getArgument('baz')); } + /** + * @dataProvider getTokenizeData + */ + public function testTokenize($input, $tokens, $message) + { + $input = new ShellInput($input); + $r = new \ReflectionClass('Psy\Input\ShellInput'); + $p = $r->getProperty('tokenPairs'); + $p->setAccessible(true); + $this->assertSame($tokens, $p->getValue($input), $message); + } + public function getTokenizeData() { // Test all the cases from StringInput test, ensuring they have an appropriate $rest token. From 5ddbc9ecce2887cb29c26d1f3d53bf09366420bd Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 3 Jun 2018 13:58:24 -0700 Subject: [PATCH 14/24] One last bit of shell input test coverage. --- test/Input/ShellInputTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/Input/ShellInputTest.php b/test/Input/ShellInputTest.php index 88ad5df5b..4a1f83dce 100644 --- a/test/Input/ShellInputTest.php +++ b/test/Input/ShellInputTest.php @@ -64,6 +64,19 @@ public function testInputOptionWithoutCodeArguments() $this->assertSame('baz\\n', $input->getArgument('baz')); } + public function testInputWithDashDash() + { + $definition = new InputDefinition([ + new InputOption('foo', null, InputOption::VALUE_REQUIRED), + new CodeArgument('code', null, CodeArgument::REQUIRED), + ]); + + $input = new ShellInput('-- echo --foo::$bar'); + $input->bind($definition); + $this->assertNull($input->getOption('foo')); + $this->assertSame('echo --foo::$bar', $input->getArgument('code')); + } + /** * @dataProvider getTokenizeData */ From c3138e1d5e7bfb68b00bf297fe76c425ef1dc313 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 3 Jun 2018 14:08:34 -0700 Subject: [PATCH 15/24] Just kidding. *This* is the last one. --- test/Input/ShellInputTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/Input/ShellInputTest.php b/test/Input/ShellInputTest.php index 4a1f83dce..f19d12618 100644 --- a/test/Input/ShellInputTest.php +++ b/test/Input/ShellInputTest.php @@ -77,6 +77,18 @@ public function testInputWithDashDash() $this->assertSame('echo --foo::$bar', $input->getArgument('code')); } + public function testInputWithEmptyString() + { + $definition = new InputDefinition([ + new InputOption('foo', null, InputOption::VALUE_REQUIRED), + new CodeArgument('code', null, CodeArgument::REQUIRED), + ]); + + $input = new ShellInput('"" --foo bar'); + $input->bind($definition); + $this->assertSame('"" --foo bar', $input->getArgument('code')); + } + /** * @dataProvider getTokenizeData */ From 35fd9ae9759e74dffdcd705c153f5e84109820a5 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Thu, 7 Jun 2018 04:48:27 +0000 Subject: [PATCH 16/24] Add a helper for getting a NoReturnValue expression. --- src/CodeCleaner/ImplicitReturnPass.php | 6 ++---- src/CodeCleaner/NoReturnValue.php | 13 ++++++++++++- test/CodeCleaner/NoReturnValueTest.php | 24 ++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 test/CodeCleaner/NoReturnValueTest.php diff --git a/src/CodeCleaner/ImplicitReturnPass.php b/src/CodeCleaner/ImplicitReturnPass.php index 60826712b..971a98045 100644 --- a/src/CodeCleaner/ImplicitReturnPass.php +++ b/src/CodeCleaner/ImplicitReturnPass.php @@ -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; @@ -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); @@ -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; diff --git a/src/CodeCleaner/NoReturnValue.php b/src/CodeCleaner/NoReturnValue.php index bca4d361d..90325701e 100644 --- a/src/CodeCleaner/NoReturnValue.php +++ b/src/CodeCleaner/NoReturnValue.php @@ -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. @@ -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')); + } } diff --git a/test/CodeCleaner/NoReturnValueTest.php b/test/CodeCleaner/NoReturnValueTest.php new file mode 100644 index 000000000..63f87a12a --- /dev/null +++ b/test/CodeCleaner/NoReturnValueTest.php @@ -0,0 +1,24 @@ +assertSame('new \\Psy\CodeCleaner\\NoReturnValue()', $this->prettyPrint($code)); + } +} From dbccd077ee41d158f7671092855d5686677934a8 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 6 Jun 2018 22:45:57 -0700 Subject: [PATCH 17/24] Normalize whitespace for ParserTestCase. --- test/ParserTestCase.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/ParserTestCase.php b/test/ParserTestCase.php index 1b39b5f15..780552068 100644 --- a/test/ParserTestCase.php +++ b/test/ParserTestCase.php @@ -65,7 +65,8 @@ protected function assertProcessesAs($from, $to) { $stmts = $this->parse($from); $stmts = $this->traverse($stmts); - $this->assertSame($to, $this->prettyPrint($stmts)); + $toStmts = $this->parse($to); + $this->assertSame($this->prettyPrint($toStmts), $this->prettyPrint($stmts)); } private function getParser() From 1ce527093da676c8ec36f8331cffe7d544a95164 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Thu, 7 Jun 2018 05:24:29 +0000 Subject: [PATCH 18/24] More accurate `timeit` profiling. Inject instrumentation into `timeit` code execution to ensure that we're not timing the overhead of parsing and processing the input, or of serializing the result. Fixes #502 --- src/Command/TimeitCommand.php | 126 +++++++++++++++++- src/Command/TimeitCommand/TimeitVisitor.php | 121 +++++++++++++++++ .../TimeitCommand/TimeitVisitorTest.php | 52 ++++++++ 3 files changed, 295 insertions(+), 4 deletions(-) create mode 100644 src/Command/TimeitCommand/TimeitVisitor.php create mode 100644 test/Command/TimeitCommand/TimeitVisitorTest.php diff --git a/src/Command/TimeitCommand.php b/src/Command/TimeitCommand.php index 1ac928150..461b7e9b5 100644 --- a/src/Command/TimeitCommand.php +++ b/src/Command/TimeitCommand.php @@ -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; @@ -25,6 +29,29 @@ class TimeitCommand extends Command const RESULT_MSG = 'Command took %.6f seconds to complete.'; const AVG_RESULT_MSG = 'Command took %.6f seconds on average (%.6f median; %.6f total) to complete.'; + 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} */ @@ -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 { @@ -76,4 +108,90 @@ 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) { + dump('YEP'); + 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 = '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 . ';'); + } + } } diff --git a/src/Command/TimeitCommand/TimeitVisitor.php b/src/Command/TimeitCommand/TimeitVisitor.php new file mode 100644 index 000000000..0f81e8231 --- /dev/null +++ b/src/Command/TimeitCommand/TimeitVisitor.php @@ -0,0 +1,121 @@ +functionDepth = 0; + } + + /** + * {@inheritdoc} + */ + public function enterNode(Node $node) + { + // keep track of nested function-like nodes, because they can have + // returns statements... and we don't want to call markEnd for those. + if ($node instanceof FunctionLike) { + $this->functionDepth++; + + return; + } + + // replace any top-level `return` statements with a `markEnd` call + if ($this->functionDepth === 0 && $node instanceof Return_) { + return new Return_($this->getEndCall($node->expr), $node->getAttributes()); + } + } + + /** + * {@inheritdoc} + */ + public function leaveNode(Node $node) + { + if ($node instanceof FunctionLike) { + $this->functionDepth--; + } + } + + /** + * {@inheritdoc} + */ + public function afterTraverse(array $nodes) + { + // prepend a `markStart` call + array_unshift($nodes, new Expression($this->getStartCall())); + + // append a `markEnd` call (wrapping the final node, if it's an expression) + $last = $nodes[count($nodes) - 1]; + if ($last instanceof Expression) { + array_pop($nodes); + $nodes[] = new Expression($this->getEndCall($last->expr), $last->getAttributes()); + } elseif ($last instanceof Return_) { + // nothing to do here, we're already ending with a return call + } else { + $nodes[] = new Expression($this->getEndCall()); + } + + return $nodes; + } + + /** + * Get PhpParser AST nodes for a `markStart` call. + * + * @return PhpParser\Node\Expr\StaticCall + */ + private function getStartCall() + { + return new StaticCall(new FullyQualifiedName('Psy\Command\TimeitCommand'), 'markStart'); + } + + /** + * Get PhpParser AST nodes for a `markEnd` call. + * + * Optionally pass in a return value. + * + * @param Expr|null $arg + * + * @return PhpParser\Node\Expr\StaticCall + */ + private function getEndCall(Expr $arg = null) + { + if ($arg === null) { + $arg = NoReturnValue::create(); + } + + return new StaticCall(new FullyQualifiedName('Psy\Command\TimeitCommand'), 'markEnd', [new Arg($arg)]); + } +} diff --git a/test/Command/TimeitCommand/TimeitVisitorTest.php b/test/Command/TimeitCommand/TimeitVisitorTest.php new file mode 100644 index 000000000..8f97c2401 --- /dev/null +++ b/test/Command/TimeitCommand/TimeitVisitorTest.php @@ -0,0 +1,52 @@ +traverser = new NodeTraverser(); + $this->traverser->addVisitor(new TimeitVisitor()); + } + + /** + * @dataProvider testCases + */ + public function testProcess($from, $to) + { + $this->assertProcessesAs($from, $to); + } + + public function testCases() + { + $start = '\Psy\Command\TimeitCommand::markStart'; + $end = '\Psy\Command\TimeitCommand::markEnd'; + $noReturn = 'new \Psy\CodeCleaner\NoReturnValue()'; + + return [ + ['', "$end($start());"], // heh + ['a()', "$start(); $end(a());"], + ['$b()', "$start(); $end(\$b());"], + ['$c->d()', "$start(); $end(\$c->d());"], + ['e(); f()', "$start(); e(); $end(f());"], + ['function g() { return 1; }', "$start(); function g() {return 1;} $end($noReturn);"], + ['return 1', "$start(); return $end(1);"], + ['return 1; 2', "$start(); return $end(1); $end(2);"], + ['return 1; function h() {}', "$start(); return $end(1); function h() {} $end($noReturn);"], + ]; + } +} From 06a2c24d5103acfea10d0b419b13cc5a6a4d16b4 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 6 Jun 2018 23:06:17 -0700 Subject: [PATCH 19/24] Remove debugging output --- src/Command/TimeitCommand.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Command/TimeitCommand.php b/src/Command/TimeitCommand.php index 461b7e9b5..70d70c0e1 100644 --- a/src/Command/TimeitCommand.php +++ b/src/Command/TimeitCommand.php @@ -152,7 +152,6 @@ public static function markEnd($ret = null) private function ensureEndMarked() { if (self::$start !== null) { - dump('YEP'); self::markEnd(); } } From 679177068e8ee530f1527a4085002b1321a2d280 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Thu, 7 Jun 2018 20:56:18 -0700 Subject: [PATCH 20/24] Update noreturnvalue test for PhpParser 3.x. --- test/CodeCleaner/NoReturnValueTest.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/CodeCleaner/NoReturnValueTest.php b/test/CodeCleaner/NoReturnValueTest.php index 63f87a12a..6a7d009a9 100644 --- a/test/CodeCleaner/NoReturnValueTest.php +++ b/test/CodeCleaner/NoReturnValueTest.php @@ -11,6 +11,7 @@ namespace Psy\Test\CodeCleaner; +use PhpParser\Node\Stmt\Expression; use Psy\CodeCleaner\NoReturnValue; use Psy\Test\ParserTestCase; @@ -18,7 +19,14 @@ class NoReturnValueTest extends ParserTestCase { public function testCreate() { - $code = [NoReturnValue::create()]; - $this->assertSame('new \\Psy\CodeCleaner\\NoReturnValue()', $this->prettyPrint($code)); + $stmt = NoReturnValue::create(); + if (class_exists('PhpParser\Node\Stmt\Expression')) { + $stmt = new Expression($stmt); + } + + $this->assertSame( + $this->prettyPrint($this->parse('new \\Psy\CodeCleaner\\NoReturnValue()')), + $this->prettyPrint([$stmt]) + ); } } From 2fbd8582a20de44d7554a2fb66675416a4c4b629 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Thu, 7 Jun 2018 20:56:58 -0700 Subject: [PATCH 21/24] Fix test data provider name that was causing issues. --- test/Command/TimeitCommand/TimeitVisitorTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Command/TimeitCommand/TimeitVisitorTest.php b/test/Command/TimeitCommand/TimeitVisitorTest.php index 8f97c2401..1f317350e 100644 --- a/test/Command/TimeitCommand/TimeitVisitorTest.php +++ b/test/Command/TimeitCommand/TimeitVisitorTest.php @@ -24,14 +24,14 @@ public function setUp() } /** - * @dataProvider testCases + * @dataProvider codez */ public function testProcess($from, $to) { $this->assertProcessesAs($from, $to); } - public function testCases() + public function codez() { $start = '\Psy\Command\TimeitCommand::markStart'; $end = '\Psy\Command\TimeitCommand::markEnd'; From a24406d4a557afab047ab61dfe318d5590471cf4 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Thu, 7 Jun 2018 20:57:38 -0700 Subject: [PATCH 22/24] Make timeit command work with PHP Parser 3.x. --- src/Command/TimeitCommand/TimeitVisitor.php | 24 ++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Command/TimeitCommand/TimeitVisitor.php b/src/Command/TimeitCommand/TimeitVisitor.php index 0f81e8231..6ee66994d 100644 --- a/src/Command/TimeitCommand/TimeitVisitor.php +++ b/src/Command/TimeitCommand/TimeitVisitor.php @@ -75,17 +75,20 @@ public function leaveNode(Node $node) public function afterTraverse(array $nodes) { // prepend a `markStart` call - array_unshift($nodes, new Expression($this->getStartCall())); + array_unshift($nodes, $this->maybeExpression($this->getStartCall())); // append a `markEnd` call (wrapping the final node, if it's an expression) $last = $nodes[count($nodes) - 1]; - if ($last instanceof Expression) { + if ($last instanceof Expr) { + array_pop($nodes); + $nodes[] = $this->getEndCall($last); + } elseif ($last instanceof Expression) { array_pop($nodes); $nodes[] = new Expression($this->getEndCall($last->expr), $last->getAttributes()); } elseif ($last instanceof Return_) { // nothing to do here, we're already ending with a return call } else { - $nodes[] = new Expression($this->getEndCall()); + $nodes[] = $this->maybeExpression($this->getEndCall()); } return $nodes; @@ -118,4 +121,19 @@ private function getEndCall(Expr $arg = null) return new StaticCall(new FullyQualifiedName('Psy\Command\TimeitCommand'), 'markEnd', [new Arg($arg)]); } + + /** + * Compatibility shim for PHP Parser 3.x. + * + * Wrap $expr in a PhpParser\Node\Stmt\Expression if the class exists. + * + * @param PhpParser\Node $expr + * @param array $attrs + * + * @return PhpParser\Node\Expr|PhpParser\Node\Stmt\Expression + */ + private function maybeExpression($expr, $attrs = []) + { + return class_exists('PhpParser\Node\Stmt\Expression') ? new Expression($expr, $attrs) : $expr; + } } From d056c066392f14e3d8bb9293a3da1e101a8d9424 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sat, 9 Jun 2018 05:05:07 +0000 Subject: [PATCH 23/24] Support omitted list items in ListPass. Fixes #503 --- src/CodeCleaner/ListPass.php | 9 ++++++++- test/CodeCleaner/ListPassTest.php | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/CodeCleaner/ListPass.php b/src/CodeCleaner/ListPass.php index 0b44082cd..04d32e9f8 100644 --- a/src/CodeCleaner/ListPass.php +++ b/src/CodeCleaner/ListPass.php @@ -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 \')\''; @@ -78,5 +81,9 @@ public function enterNode(Node $node) throw new ParseErrorException($msg, $item->getLine()); } } + + if (!$itemFound) { + throw new ParseErrorException('Cannot use empty list'); + } } } diff --git a/test/CodeCleaner/ListPassTest.php b/test/CodeCleaner/ListPassTest.php index 06954cb11..95cf3a744 100644 --- a/test/CodeCleaner/ListPassTest.php +++ b/test/CodeCleaner/ListPassTest.php @@ -63,6 +63,7 @@ public function invalidStatements() ['list("a" => _) = array("a" => 1)', $errorPhpParserSyntax], ['["a"] = [1]', $errorNonVariableAssign], ['[] = []', $errorEmptyList], + ['[,] = [1,2]', $errorEmptyList], ]); } @@ -91,6 +92,10 @@ public function validStatements() ['[$a] = [1]'], ['[$x, $y] = [1, 2]'], ['["_" => $v] = ["_" => 1]'], + ['[$a,] = [1,2,3]'], + ['[,$b] = [1,2,3]'], + ['[$a,,$c] = [1,2,3]'], + ['[$a,,,] = [1,2,3]'], ]); } From 6d4e905461e9eda75b0e0f8400dc405635979e03 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 10 Jun 2018 10:56:56 -0700 Subject: [PATCH 24/24] Bump to v0.9.6 --- src/Shell.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shell.php b/src/Shell.php index 0c333d780..3fe0b761c 100644 --- a/src/Shell.php +++ b/src/Shell.php @@ -47,7 +47,7 @@ */ class Shell extends Application { - const VERSION = 'v0.9.5'; + const VERSION = 'v0.9.6'; const PROMPT = '>>> '; const BUFF_PROMPT = '... ';