Update Composer, update everything

This commit is contained in:
Oliver Davies 2018-11-23 12:29:20 +00:00
parent ea3e94409f
commit dda5c284b6
19527 changed files with 1135420 additions and 351004 deletions

15
vendor/psy/psysh/.editorconfig vendored Normal file
View file

@ -0,0 +1,15 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

View file

@ -0,0 +1,9 @@
## Code style
Please make your code look like the other code in the project.
PsySH follows [PSR-1](http://php-fig.org/psr/psr-1/) and [PSR-2](http://php-fig.org/psr/psr-2/). The easiest way to do make sure you're following the coding standard is to [install `php-cs-fixer`](https://github.com/friendsofphp/php-cs-fixer) and run `php-cs-fixer fix` before committing.
## Branching model
Please branch off and send pull requests to the `develop` branch.

9
vendor/psy/psysh/.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
/build/
/dist/
/composer.lock
/manual/
/psysh
/__pycache__
/.php_cs.cache
/vendor/
/vendor-bin/*/vendor/

46
vendor/psy/psysh/.phan/config.php vendored Normal file
View file

@ -0,0 +1,46 @@
<?php
/**
* This configuration will be read and overlaid on top of the
* default configuration. Command line arguments will be applied
* after this file is read.
*/
return [
// A list of directories that should be parsed for class and
// method information. After excluding the directories
// defined in exclude_analysis_directory_list, the remaining
// files will be statically analyzed for errors.
//
// Thus, both first-party and third-party code being used by
// your application should be included in this list.
'directory_list' => [
'src/',
'vendor/dnoegel/php-xdg-base-dir/src/',
'vendor/doctrine/instantiator/src/',
'vendor/hoa/console/',
'vendor/jakub-onderka/php-console-color/src/',
'vendor/jakub-onderka/php-console-highlighter/src/',
'vendor/nikic/php-parser/lib/',
'vendor/phpdocumentor/reflection-docblock/',
'vendor/symfony/console/',
'vendor/symfony/filesystem/',
'vendor/symfony/finder/',
'vendor/symfony/var-dumper/',
],
// A directory list that defines files that will be excluded
// from static analysis, but whose class and method
// information should be included.
//
// Generally, you'll want to include the directories for
// third-party code (such as "vendor/") in this list.
//
// n.b.: If you'd like to parse but not analyze 3rd
// party code, directories containing that code
// should be added to both the `directory_list`
// and `exclude_analysis_directory_list` arrays.
"exclude_analysis_directory_list" => [
'vendor/'
],
];

32
vendor/psy/psysh/.php_cs vendored Normal file
View file

@ -0,0 +1,32 @@
<?php
$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
->name('.php_cs')
->name('build-manual')
->name('build-phar')
->exclude('build-vendor');
$header = <<<EOF
This file is part of Psy Shell.
(c) 2012-2018 Justin Hileman
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
EOF;
return PhpCsFixer\Config::create()
->setRules(array(
'@Symfony' => true,
'array_syntax' => array('syntax' => 'short'),
'binary_operator_spaces' => false,
'concat_space' => array('spacing' => 'one'),
'header_comment' => array('header' => $header),
'increment_style' => array('style' => 'post'),
'method_argument_space' => array('keep_multiple_spaces_after_comma' => true),
'ordered_imports' => true,
'pre_increment' => false,
'yoda_style' => false,
))
->setFinder($finder);

29
vendor/psy/psysh/.styleci.yml vendored Normal file
View file

@ -0,0 +1,29 @@
preset: symfony
enabled:
- align_double_arrow
- concat_with_spaces
- short_array_syntax
- ordered_use
- strict
disabled:
- blank_line_before_break
- blank_line_before_continue
- blank_line_before_throw
- blank_line_before_try
- concat_without_spaces
- method_argument_space
- pre_increment
- unalign_double_arrow
- unalign_equals
- yoda_style
- property_separation
- const_separation
finder:
name:
- "*.php"
- ".php_cs"
- "build-manual"
- "build-phar"

44
vendor/psy/psysh/.travis.yml vendored Normal file
View file

@ -0,0 +1,44 @@
language: php
sudo: false
matrix:
include:
- php: 5.4
- php: 5.4
env: 'COMPOSER_FLAGS="--prefer-lowest --prefer-stable"'
- php: 5.5
- php: 5.6
- php: 7.0
- php: 7.1
- php: 7.2
- php: hhvm
dist: trusty
allow_failures:
- php: 5.4
env: 'COMPOSER_FLAGS="--prefer-lowest --prefer-stable"'
- php: hhvm
fast_finish: true
install: travis_retry composer update --no-interaction $COMPOSER_FLAGS
script:
- vendor/bin/phpunit --verbose --coverage-clover=coverage.xml
- '[[ $TRAVIS_PHP_VERSION = 7.2* ]] && make build -j 4 || true'
after_success:
- bash <(curl -s https://codecov.io/bash)
before_deploy: make dist -j 4
deploy:
provider: releases
api_key:
secure: LL8koDM1xDqzF9t0URHvmMPyWjojyd4PeZ7IW7XYgyvD6n1H6GYrVAeKCh5wfUKFbwHoa9s5AAn6pLzra00bODVkPTmUH+FSMWz9JKLw9ODAn8HvN7C+IooxmeClGHFZc0TfHfya8/D1E9C1iXtGGEoE/GqtaYq/z0C1DLpO0OU=
file_glob: true
file: dist/psysh-*.tar.gz
skip_cleanup: true
on:
tags: true
repo: bobthecow/psysh
condition: $TRAVIS_PHP_VERSION = 7.2*

21
vendor/psy/psysh/LICENSE vendored Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2012-2018 Justin Hileman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.

95
vendor/psy/psysh/Makefile vendored Normal file
View file

@ -0,0 +1,95 @@
PSYSH_SRC = bin src box.json.dist composer.json build/stub
PSYSH_SRC_FILES = $(shell find src -type f -name "*.php")
VERSION = $(shell git describe --tag --always --dirty=-dev)
COMPOSER_OPTS = --no-interaction --no-progress --verbose
COMPOSER_REQUIRE_OPTS = $(COMPOSER_OPTS) --no-update
COMPOSER_UPDATE_OPTS = $(COMPOSER_OPTS) --prefer-stable --no-dev --classmap-authoritative --prefer-dist
# Commands
.PHONY: help clean build dist
.DEFAULT_GOAL := help
help:
@echo "\033[33mUsage:\033[0m\n make TARGET\n\n\033[33mTargets:\033[0m"
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[32m%-7s\033[0m %s\n", $$1, $$2}'
clean: ## Clean all created artifacts
rm -rf build/*
rm -rf dist/*
rm -rf vendor-bin/*/vendor/
build: ## Compile PHARs
build: build/psysh/psysh build/psysh-compat/psysh build/psysh-php54/psysh build/psysh-php54-compat/psysh
dist: ## Build tarballs for distribution
dist: dist/psysh-$(VERSION).tar.gz dist/psysh-$(VERSION)-compat.tar.gz dist/psysh-$(VERSION)-php54.tar.gz dist/psysh-$(VERSION)-php54-compat.tar.gz
# All the composer stuffs
composer.lock: composer.json
composer install
touch $@
vendor/autoload.php: composer.lock
composer install
touch $@
vendor/bin/box: vendor/autoload.php
composer bin box install
touch $@
# Lots of PHARs
build/stub: bin/build-stub bin/psysh LICENSE
bin/build-stub
build/psysh: $(PSYSH_SRC) $(PSYSH_SRC_FILES)
rm -rf $@ || true
mkdir $@
cp -R $(PSYSH_SRC) $@/
composer config --working-dir $@ platform.php 7.0
composer require --working-dir $@ $(COMPOSER_REQUIRE_OPTS) php:'>=7.0.0'
composer update --working-dir $@ $(COMPOSER_UPDATE_OPTS)
build/psysh-compat: $(PSYSH_SRC) $(PSYSH_SRC_FILES)
rm -rf $@ || true
mkdir $@
cp -R $(PSYSH_SRC) $@/
composer config --working-dir $@ platform.php 7.0
composer require --working-dir $@ $(COMPOSER_REQUIRE_OPTS) php:'>=7.0.0'
composer require --working-dir $@ $(COMPOSER_REQUIRE_OPTS) symfony/polyfill-iconv symfony/polyfill-mbstring hoa/console
composer update --working-dir $@ $(COMPOSER_UPDATE_OPTS)
build/psysh-php54: $(PSYSH_SRC) $(PSYSH_SRC_FILES)
rm -rf $@ || true
mkdir $@
cp -R $(PSYSH_SRC) $@/
composer config --working-dir $@ platform.php 5.4
composer update --working-dir $@ $(COMPOSER_UPDATE_OPTS)
build/psysh-php54-compat: $(PSYSH_SRC) $(PSYSH_SRC_FILES)
rm -rf $@ || true
mkdir $@
cp -R $(PSYSH_SRC) $@/
composer config --working-dir $@ platform.php 5.4
composer require --working-dir $@ $(COMPOSER_REQUIRE_OPTS) symfony/polyfill-iconv symfony/polyfill-mbstring hoa/console:^2.15
composer update --working-dir $@ $(COMPOSER_UPDATE_OPTS)
build/%/psysh: vendor/bin/box build/%
vendor/bin/box compile --working-dir $(dir $@)
# Dist packages
dist/psysh-$(VERSION).tar.gz: build/psysh/psysh
@mkdir -p $(@D)
tar -C $(dir $<) -czf $@ $(notdir $<)
dist/psysh-$(VERSION)-%.tar.gz: build/psysh-%/psysh
@mkdir -p $(@D)
tar -C $(dir $<) -czf $@ $(notdir $<)

34
vendor/psy/psysh/README.md vendored Normal file
View file

@ -0,0 +1,34 @@
# PsySH
PsySH is a runtime developer console, interactive debugger and [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) for PHP. Learn more at [psysh.org](http://psysh.org/) and [in the manual](https://github.com/bobthecow/psysh/wiki/Home).
[![Package version](https://img.shields.io/packagist/v/psy/psysh.svg?style=flat-square)](https://packagist.org/packages/psy/psysh)
[![Monthly downloads](http://img.shields.io/packagist/dm/psy/psysh.svg?style=flat-square)](https://packagist.org/packages/psy/psysh)
[![Made out of awesome](https://img.shields.io/badge/made_out_of_awesome-✓-brightgreen.svg?style=flat-square)](http://psysh.org)
[![Build status](https://img.shields.io/travis/bobthecow/psysh/master.svg?style=flat-square)](http://travis-ci.org/bobthecow/psysh)
[![StyleCI](https://styleci.io/repos/4549925/shield)](https://styleci.io/repos/4549925)
<a id="downloading-the-manual"></a>
## [PsySH manual](https://github.com/bobthecow/psysh/wiki/Home)
### [💾 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)
* [⏳ Managing history](https://github.com/bobthecow/psysh/wiki/History)
* [💲 System shell integration](https://github.com/bobthecow/psysh/wiki/Shell-integration)
* [🎥 Tutorials & guides](https://github.com/bobthecow/psysh/wiki/Tutorials)
### [📢 Commands](https://github.com/bobthecow/psysh/wiki/Commands)
### [🛠 Configuration](https://github.com/bobthecow/psysh/wiki/Configuration)
* [🎛 Config options](https://github.com/bobthecow/psysh/wiki/Config-options)
* [📄 Sample config file](https://github.com/bobthecow/psysh/wiki/Sample-config)
### [🔌 Integrations](https://github.com/bobthecow/psysh/wiki/Integrations)

22
vendor/psy/psysh/bin/build-stub vendored Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env php
<?php
$license = file_get_contents(dirname(__DIR__) . '/LICENSE');
$license = str_replace('The MIT License (MIT)', '', $license);
$license = str_replace("\n", "\n * ", trim($license));
$autoload = <<<'EOS'
Phar::mapPhar('psysh.phar');
require 'phar://psysh.phar/.box/check_requirements.php';
require 'phar://psysh.phar/vendor/autoload.php';
EOS;
$content = file_get_contents(dirname(__DIR__) . '/bin/psysh');
$content = preg_replace('{/\* <<<.*?>>> \*/}sm', $autoload, $content);
$content = preg_replace('/\\(c\\) .*?with this source code./sm', $license, $content);
$content .= '__HALT_COMPILER();';
@mkdir(dirname(__DIR__) . '/build');
file_put_contents(dirname(__DIR__) . '/build/stub', $content);

138
vendor/psy/psysh/bin/psysh vendored Executable file
View file

@ -0,0 +1,138 @@
#!/usr/bin/env php
<?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.
*/
// Try to find an autoloader for a local psysh version.
// We'll wrap this whole mess in a Closure so it doesn't leak any globals.
call_user_func(function () {
$cwd = null;
// Find the cwd arg (if present)
$argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : array();
foreach ($argv as $i => $arg) {
if ($arg === '--cwd') {
if ($i >= count($argv) - 1) {
echo 'Missing --cwd argument.' . PHP_EOL;
exit(1);
}
$cwd = $argv[$i + 1];
break;
}
if (preg_match('/^--cwd=/', $arg)) {
$cwd = substr($arg, 6);
break;
}
}
// Or fall back to the actual cwd
if (!isset($cwd)) {
$cwd = getcwd();
}
$cwd = str_replace('\\', '/', $cwd);
$chunks = explode('/', $cwd);
while (!empty($chunks)) {
$path = implode('/', $chunks);
// Find composer.json
if (is_file($path . '/composer.json')) {
if ($cfg = json_decode(file_get_contents($path . '/composer.json'), true)) {
if (isset($cfg['name']) && $cfg['name'] === 'psy/psysh') {
// We're inside the psysh project. Let's use the local
// Composer autoload.
if (is_file($path . '/vendor/autoload.php')) {
require $path . '/vendor/autoload.php';
}
return;
}
}
}
// Or a composer.lock
if (is_file($path . '/composer.lock')) {
if ($cfg = json_decode(file_get_contents($path . '/composer.lock'), true)) {
foreach (array_merge($cfg['packages'], $cfg['packages-dev']) as $pkg) {
if (isset($pkg['name']) && $pkg['name'] === 'psy/psysh') {
// We're inside a project which requires psysh. We'll
// use the local Composer autoload.
if (is_file($path . '/vendor/autoload.php')) {
require $path . '/vendor/autoload.php';
}
return;
}
}
}
}
array_pop($chunks);
}
});
// We didn't find an autoloader for a local version, so use the autoloader that
// came with this script.
if (!class_exists('Psy\Shell')) {
/* <<< */
if (is_file(__DIR__ . '/../vendor/autoload.php')) {
require __DIR__ . '/../vendor/autoload.php';
} elseif (is_file(__DIR__ . '/../../../autoload.php')) {
require __DIR__ . '/../../../autoload.php';
} else {
echo 'PsySH dependencies not found, be sure to run `composer install`.' . PHP_EOL;
echo 'See https://getcomposer.org to get Composer.' . PHP_EOL;
exit(1);
}
/* >>> */
}
// If the psysh binary was included directly, assume they just wanted an
// autoloader and bail early.
//
// Keep this PHP 5.3 code around for a while in case someone is using a globally
// installed psysh as a bin launcher for older local versions.
if (version_compare(PHP_VERSION, '5.3.6', '<')) {
$trace = debug_backtrace();
} elseif (version_compare(PHP_VERSION, '5.4.0', '<')) {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
} else {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
}
if (Psy\Shell::isIncluded($trace)) {
unset($trace);
return;
}
// Clean up after ourselves.
unset($trace);
// If the local version is too old, we can't do this
if (!function_exists('Psy\bin')) {
$argv = $_SERVER['argv'];
$first = array_shift($argv);
if (preg_match('/php(\.exe)?$/', $first)) {
array_shift($argv);
}
array_unshift($argv, 'vendor/bin/psysh');
echo 'A local PsySH dependency was found, but it cannot be loaded. Please update to' . PHP_EOL;
echo 'the latest version, or run the local copy directly, e.g.:' . PHP_EOL;
echo PHP_EOL;
echo ' ' . implode(' ', $argv) . PHP_EOL;
exit(1);
}
// And go!
call_user_func(Psy\bin());

12
vendor/psy/psysh/box.json.dist vendored Normal file
View file

@ -0,0 +1,12 @@
{
"stub": "stub",
"output": "psysh",
"compactors": [
"KevinGH\\Box\\Compactor\\Php"
],
"blacklist": [
"grammar",
"test_old",
"Documentation"
]
}

54
vendor/psy/psysh/composer.json vendored Normal file
View file

@ -0,0 +1,54 @@
{
"name": "psy/psysh",
"description": "An interactive shell for modern PHP.",
"type": "library",
"keywords": ["console", "interactive", "shell", "repl"],
"homepage": "http://psysh.org",
"license": "MIT",
"authors": [
{
"name": "Justin Hileman",
"email": "justin@justinhileman.info",
"homepage": "http://justinhileman.com"
}
],
"require": {
"php": ">=5.4.0",
"ext-json": "*",
"ext-tokenizer": "*",
"symfony/console": "~2.3.10|^2.4.2|~3.0|~4.0",
"symfony/var-dumper": "~2.7|~3.0|~4.0",
"nikic/php-parser": "~1.3|~2.0|~3.0|~4.0",
"dnoegel/php-xdg-base-dir": "0.1",
"jakub-onderka/php-console-highlighter": "0.3.*|0.4.*"
},
"require-dev": {
"phpunit/phpunit": "~4.8.35|~5.0|~6.0|~7.0",
"hoa/console": "~2.15|~3.16",
"bamarni/composer-bin-plugin": "^1.2"
},
"suggest": {
"ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)",
"ext-posix": "If you have PCNTL, you'll want the POSIX extension as well.",
"ext-readline": "Enables support for arrow-key history navigation, and showing and manipulating command history.",
"ext-pdo-sqlite": "The doc command requires SQLite to work.",
"hoa/console": "A pure PHP readline implementation. You'll want this if your PHP install doesn't already support readline or libedit."
},
"autoload": {
"files": ["src/functions.php"],
"psr-4": {
"Psy\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Psy\\Test\\": "test/"
}
},
"bin": ["bin/psysh"],
"extra": {
"branch-alias": {
"dev-develop": "0.9.x-dev"
}
}
}

12
vendor/psy/psysh/phpunit.xml.dist vendored Normal file
View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false" colors="true" bootstrap="vendor/autoload.php">
<testsuite name="PsySH">
<directory suffix="Test.php">./test</directory>
</testsuite>
<filter>
<whitelist>
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
</phpunit>

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

@ -0,0 +1,349 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\ListPass;
use Psy\CodeCleaner\LoopContextPass;
use Psy\CodeCleaner\MagicConstantsPass;
use Psy\CodeCleaner\NamespacePass;
use Psy\CodeCleaner\PassableByReferencePass;
use Psy\CodeCleaner\RequirePass;
use Psy\CodeCleaner\StrictTypesPass;
use Psy\CodeCleaner\UseStatementPass;
use Psy\CodeCleaner\ValidClassNamePass;
use Psy\CodeCleaner\ValidConstantPass;
use Psy\CodeCleaner\ValidConstructorPass;
use Psy\CodeCleaner\ValidFunctionNamePass;
use Psy\Exception\ParseErrorException;
/**
* A service to clean up user input, detect parse errors before they happen,
* and generally work around issues with the PHP code evaluation experience.
*/
class CodeCleaner
{
private $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()
{
$useStatementPass = new UseStatementPass();
$namespacePass = new NamespacePass($this);
// Try to add implicit `use` statements and an implicit namespace,
// based on the file in which the `debug` call was made.
$this->addImplicitDebugContext([$useStatementPass, $namespacePass]);
return [
// Validation passes
new AbstractClassPass(),
new AssignThisVariablePass(),
new CalledClassPass(),
new CallTimePassByReferencePass(),
new FinalClassPass(),
new FunctionContextPass(),
new FunctionReturnInWriteContextPass(),
new InstanceOfPass(),
new LeavePsyshAlonePass(),
new LegacyEmptyPass(),
new ListPass(),
new LoopContextPass(),
new PassableByReferencePass(),
new ValidConstructorPass(),
// Rewriting shenanigans
$useStatementPass, // must run before the namespace pass
new ExitPass(),
new ImplicitReturnPass(),
new MagicConstantsPass(),
$namespacePass, // must run after the implicit return pass
new RequirePass(),
new StrictTypesPass(),
// Namespace-aware validation (which depends on aforementioned shenanigans)
new ValidClassNamePass(),
new ValidConstantPass(),
new ValidFunctionNamePass(),
];
}
/**
* "Warm up" code cleaner passes when we're coming from a debug call.
*
* This is useful, for example, for `UseStatementPass` and `NamespacePass`
* which keep track of state between calls, to maintain the current
* namespace and a map of use statements.
*
* @param array $passes
*/
private function addImplicitDebugContext(array $passes)
{
$file = $this->getDebugFile();
if ($file === null) {
return;
}
try {
$code = @\file_get_contents($file);
if (!$code) {
return;
}
$stmts = $this->parse($code, true);
if ($stmts === false) {
return;
}
// Set up a clean traverser for just these code cleaner passes
$traverser = new NodeTraverser();
foreach ($passes as $pass) {
$traverser->addVisitor($pass);
}
$traverser->traverse($stmts);
} catch (\Throwable $e) {
// Don't care.
} catch (\Exception $e) {
// Still don't care.
}
}
/**
* Search the stack trace for a file in which the user called Psy\debug.
*
* @return string|null
*/
private static function getDebugFile()
{
$trace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
foreach (\array_reverse($trace) as $stackFrame) {
if (!self::isDebugCall($stackFrame)) {
continue;
}
if (\preg_match('/eval\(/', $stackFrame['file'])) {
\preg_match_all('/([^\(]+)\((\d+)/', $stackFrame['file'], $matches);
return $matches[1][0];
}
return $stackFrame['file'];
}
}
/**
* Check whether a given backtrace frame is a call to Psy\debug.
*
* @param array $stackFrame
*
* @return bool
*/
private static function isDebugCall(array $stackFrame)
{
$class = isset($stackFrame['class']) ? $stackFrame['class'] : null;
$function = isset($stackFrame['function']) ? $stackFrame['function'] : null;
return ($class === null && $function === 'Psy\debug') ||
($class === 'Psy\Shell' && $function === 'debug');
}
/**
* Clean the given array of code.
*
* @throws ParseErrorException if the code is invalid PHP, and cannot be coerced into valid PHP
*
* @param array $codeLines
* @param bool $requireSemicolons
*
* @return string|false Cleaned PHP code, False if the input is incomplete
*/
public function clean(array $codeLines, $requireSemicolons = false)
{
$stmts = $this->parse('<?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-2018 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 = [];
} 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-2018 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,50 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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
*
* @param Node $node
*/
public function enterNode(Node $node)
{
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-2018 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, ['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-2018 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-2018 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-2018 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 = [];
}
/**
* @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-2018 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-2018 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 $atLeastPhp55;
public function __construct()
{
$this->atLeastPhp55 = \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->atLeastPhp55) ? self::PHP55_MESSAGE : self::EXCEPTION_MESSAGE;
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
} elseif ($node instanceof Empty_ && !$this->atLeastPhp55 && $this->isCallNode($node->expr)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine()); // @codeCoverageIgnore
} 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,128 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Exit_;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\Expression;
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 [new Return_(NoReturnValue::create())];
}
$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_)) {
// @codeCoverageIgnoreStart
$nodes[\count($nodes) - 1] = new Return_($last, [
'startLine' => $last->getLine(),
'endLine' => $last->getLine(),
]);
// @codeCoverageIgnoreEnd
} elseif ($last instanceof Expression && !($last->expr instanceof Exit_)) {
// For PHP Parser 4.x
$nodes[\count($nodes) - 1] = new Return_($last->expr, [
'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 (self::isNonExpressionStmt($last)) {
$nodes[] = new Return_(NoReturnValue::create());
}
return $nodes;
}
/**
* Check whether a given node is a non-expression statement.
*
* As of PHP Parser 4.x, Expressions are now instances of Stmt as well, so
* we'll exclude them here.
*
* @param Node $node
*
* @return bool
*/
private static function isNonExpressionStmt(Node $node)
{
return $node instanceof Stmt &&
!$node instanceof Expression &&
!$node instanceof Return_ &&
!$node instanceof Namespace_;
}
}

View file

@ -0,0 +1,47 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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-2018 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,73 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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.
*
* @codeCoverageIgnore
*/
class LegacyEmptyPass extends CodeCleanerPass
{
private $atLeastPhp55;
public function __construct()
{
$this->atLeastPhp55 = \version_compare(PHP_VERSION, '5.5', '>=');
}
/**
* 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 ($this->atLeastPhp55) {
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,112 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Array_;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\List_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use Psy\Exception\ParseErrorException;
/**
* Validate that the list assignment.
*/
class ListPass extends CodeCleanerPass
{
private $atLeastPhp71;
public function __construct()
{
$this->atLeastPhp71 = \version_compare(PHP_VERSION, '7.1', '>=');
}
/**
* Validate use of list assignment.
*
* @throws ParseErrorException if the user used empty with anything but a variable
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if (!$node instanceof Assign) {
return;
}
if (!$node->var instanceof Array_ && !$node->var instanceof List_) {
return;
}
if (!$this->atLeastPhp71 && $node->var instanceof Array_) {
$msg = "syntax error, unexpected '='";
throw new ParseErrorException($msg, $node->expr->getLine());
}
// Polyfill for PHP-Parser 2.x
$items = isset($node->var->items) ? $node->var->items : $node->var->vars;
if ($items === [] || $items === [null]) {
throw new ParseErrorException('Cannot use empty list', $node->var->getLine());
}
$itemFound = false;
foreach ($items as $item) {
if ($item === null) {
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 \')\'';
throw new ParseErrorException($msg, $item->key->getLine());
}
if (!self::isValidArrayItem($item)) {
$msg = 'Assignments can only happen to writable values';
throw new ParseErrorException($msg, $item->getLine());
}
}
if (!$itemFound) {
throw new ParseErrorException('Cannot use empty list');
}
}
/**
* Validate whether a given item in an array is valid for short assignment.
*
* @param Expr $item
*
* @return bool
*/
private static function isValidArrayItem(Expr $item)
{
$value = ($item instanceof ArrayItem) ? $item->value : $item;
while ($value instanceof ArrayDimFetch || $value instanceof PropertyFetch) {
$value = $value->var;
}
// We just kind of give up if it's a method call. We can't tell if it's
// valid via static analysis.
return $value instanceof Variable || $value instanceof MethodCall || $value instanceof FuncCall;
}
}

View file

@ -0,0 +1,103 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 $loopDepth;
/**
* {@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 ($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) {
$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-2018 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'), [], $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-2018 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 = [];
$this->currentScope = [];
}
/**
* @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 : [];
}
}
/**
* 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 = [$name];
}
return \implode('\\', \array_merge($this->namespace, $name));
}
}

View file

@ -0,0 +1,88 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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_) {
$kind = $last->getAttribute('kind');
// Treat all namespace statements pre-PHP-Parser v3.1.2 as "open",
// even though we really have no way of knowing.
if ($kind === null || $kind === Namespace_::KIND_SEMICOLON) {
// Save the current namespace for open namespaces
$this->setNamespace($last->name);
} else {
// Clear the current namespace after a braced namespace
$this->setNamespace(null);
}
return $nodes;
}
return $this->namespace ? [new Namespace_($this->namespace, $nodes)] : $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,35 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\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.
*
* 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
{
/**
* 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'));
}
}

View file

@ -0,0 +1,109 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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,101 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 = [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',
[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);
}
// @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,87 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Identifier;
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;
private $atLeastPhp7;
public function __construct()
{
$this->atLeastPhp7 = \version_compare(PHP_VERSION, '7.0', '>=');
}
/**
* 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 (!$this->atLeastPhp7) {
return; // @codeCoverageIgnore
}
$prependStrictTypes = $this->strictTypes;
foreach ($nodes as $key => $node) {
if ($node instanceof Declare_) {
foreach ($node->declares as $declare) {
// For PHP Parser 4.x
$declareKey = $declare->key instanceof Identifier ? $declare->key->toString() : $declare->key;
if ($declareKey === '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_([new DeclareDeclare('strict_types', new LNumber(1))]);
\array_unshift($nodes, $declare);
}
}
return $nodes;
}
}

View file

@ -0,0 +1,126 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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_;
use PhpParser\NodeTraverser;
/**
* 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 = [];
private $lastAliases = [];
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) {
$alias = $use->alias ?: \end($use->name->parts);
$this->aliases[\strtolower($alias)] = $use->name;
}
return NodeTraverser::REMOVE_NODE;
} 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) {
$alias = $use->alias ?: \end($use->name->parts);
$this->aliases[\strtolower($alias)] = Name::concat($node->prefix, $use->name, [
'startLine' => $node->prefix->getAttribute('startLine'),
'endLine' => $use->name->getAttribute('endLine'),
]);
}
return NodeTraverser::REMOVE_NODE;
} elseif ($node instanceof Namespace_) {
// Start fresh, since we're done with this namespace.
$this->lastNamespace = $node->name;
$this->lastAliases = $this->aliases;
$this->aliases = [];
} 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,411 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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';
private $conditionalScopes = 0;
private $atLeastPhp55;
public function __construct()
{
$this->atLeastPhp55 = \version_compare(PHP_VERSION, '5.5', '>=');
}
/**
* 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 $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, self::CLASS_TYPE);
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, self::INTERFACE_TYPE);
$this->ensureInterfacesExist($stmt->extends, $stmt);
}
/**
* Validate a trait definition statement.
*
* @param Trait_ $stmt
*/
protected function validateTraitStatement(Trait_ $stmt)
{
$this->ensureCanDefine($stmt, self::TRAIT_TYPE);
}
/**
* 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' && $this->atLeastPhp55) {
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
* @param string $scopeType
*/
protected function ensureCanDefine(Stmt $stmt, $scopeType = self::CLASS_TYPE)
{
$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)] = $scopeType;
}
/**
* 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 referenced class _or trait_ exists.
*
* @throws FatalErrorException
*
* @param string $name
* @param Stmt $stmt
*/
protected function ensureClassOrTraitExists($name, $stmt)
{
if (!$this->classExists($name) && !$this->traitExists($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->ensureClassOrTraitExists($class, $stmt);
// let's pretend all calls to self, parent and static are valid
if (\in_array(\strtolower($class), ['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 Interface_[] $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.
*
* @deprecated No longer used. Scope type should be passed into ensureCanDefine directly.
* @codeCoverageIgnore
*
* @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), ['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 \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,90 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 PhpParser\Node\Identifier;
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)
{
// For PHP Parser 4.x
$constName = $stmt->name instanceof Identifier ? $stmt->name->toString() : $stmt->name;
// give the `class` pseudo-constant a pass
if ($constName === '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($constName)) {
$constType = \class_exists($className) ? 'Class' : 'Interface';
$msg = \sprintf('%s constant \'%s::%s\' not found', $constType, $className, $constName);
throw new FatalErrorException($msg, 0, E_ERROR, null, $stmt->getLine());
}
}
}
}
}

View file

@ -0,0 +1,112 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Identifier;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Namespace_;
use Psy\Exception\FatalErrorException;
/**
* Validate that the constructor method is not static, and does not have a
* return type.
*
* Checks both explicit __construct methods as well as old-style constructor
* methods with the same name as the class (for non-namespaced classes).
*
* 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.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
class ValidConstructorPass extends CodeCleanerPass
{
private $namespace;
public function beforeTraverse(array $nodes)
{
$this->namespace = [];
}
/**
* Validate that the constructor is not static and does not have a return type.
*
* @throws FatalErrorException the constructor function is static
* @throws FatalErrorException the constructor function has a return type
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if ($node instanceof Namespace_) {
$this->namespace = isset($node->name) ? $node->name->parts : [];
} elseif ($node instanceof Class_) {
$constructor = null;
foreach ($node->stmts as $stmt) {
if ($stmt instanceof ClassMethod) {
// If we find a new-style constructor, no need to look for the old-style
if ('__construct' === \strtolower($stmt->name)) {
$this->validateConstructor($stmt, $node);
return;
}
// We found a possible old-style constructor (unless there is also a __construct method)
if (empty($this->namespace) && \strtolower($node->name) === \strtolower($stmt->name)) {
$constructor = $stmt;
}
}
}
if ($constructor) {
$this->validateConstructor($constructor, $node);
}
}
}
/**
* @throws FatalErrorException the constructor function is static
* @throws FatalErrorException the constructor function has a return type
*
* @param Node $constructor
* @param Node $classNode
*/
private function validateConstructor(Node $constructor, Node $classNode)
{
if ($constructor->isStatic()) {
// For PHP Parser 4.x
$className = $classNode->name instanceof Identifier ? $classNode->name->toString() : $classNode->name;
$msg = \sprintf(
'Constructor %s::%s() cannot be static',
\implode('\\', \array_merge($this->namespace, (array) $className)),
$constructor->name
);
throw new FatalErrorException($msg, 0, E_ERROR, null, $classNode->getLine());
}
if (\method_exists($constructor, 'getReturnType') && $constructor->getReturnType()) {
// For PHP Parser 4.x
$className = $classNode->name instanceof Identifier ? $classNode->name->toString() : $classNode->name;
$msg = \sprintf(
'Constructor %s::%s() cannot declare a return type',
\implode('\\', \array_merge($this->namespace, (array) $className)),
$constructor->name
);
throw new FatalErrorException($msg, 0, E_ERROR, null, $classNode->getLine());
}
}
}

View file

@ -0,0 +1,97 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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-2018 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(['buf'])
->setDefinition([
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-2018 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([])
->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));
}
}

282
vendor/psy/psysh/src/Command/Command.php vendored Normal file
View file

@ -0,0 +1,282 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 = [
'<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 ['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 ['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 = [];
$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 = [];
$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([])
->setStyle($style);
}
/**
* Legacy fallback for getTable.
*
* @return TableHelper
*/
protected function getTableHelper()
{
$table = $this->getApplication()->getHelperSet()->get('table');
return $table
->setRows([])
->setLayout(TableHelper::LAYOUT_BORDERLESS)
->setHorizontalBorderChar('')
->setCrossingChar('');
}
}

View file

@ -0,0 +1,131 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Input\CodeArgument;
use Psy\Reflection\ReflectionLanguageConstruct;
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(['rtfm', 'man'])
->setDefinition([
new CodeArgument('target', CodeArgument::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('target');
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;
case 'ReflectionClassConstant':
case 'Psy\Reflection\ReflectionClassConstant':
// @todo this is going to collide with ReflectionMethod ids
// someday... start running the query by id + type if the DB
// supports it.
$id = $reflector->class . '::' . $reflector->name;
break;
case 'Psy\Reflection\ReflectionConstant_':
$id = $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,94 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\CodeArgument;
use Psy\VarDumper\Presenter;
use Psy\VarDumper\PresenterAware;
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([
new CodeArgument('target', CodeArgument::REQUIRED, 'A target object or primitive to dump.'),
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>
<return>>>> dump $stuff->getAll()</return>
HELP
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$depth = $input->getOption('depth');
$target = $this->resolveCode($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));
}
}
/**
* @deprecated Use `resolveCode` instead
*
* @param string $name
*
* @return mixed
*/
protected function resolveTarget($name)
{
@\trigger_error('`resolveTarget` is deprecated; use `resolveCode` instead.', E_USER_DEPRECATED);
return $this->resolveCode($name);
}
}

View file

@ -0,0 +1,187 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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([
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 = [];
$proc = \proc_open((\getenv('EDITOR') ?: 'nano') . " {$escapedFilePath}", [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-2018 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(['quit', 'q'])
->setDefinition([])
->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-2018 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(['?'])
->setDefinition([
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([
\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,246 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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;
private $readline;
/**
* {@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(['hist'])
->setDefinition([
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, ['show', 'head', 'tail']);
$this->validateOnlyOne($input, ['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 = [];
$highlighted = [];
foreach ($history as $i => $line) {
if ($this->filter->match($line, $matches)) {
if (isset($matches[0])) {
$chunks = \explode($matches[0], $history[$i]);
$chunks = \array_map([__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 [$range, $range + 1];
}
$matches = [];
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 [$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,276 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\CodeArgument;
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\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 $presenter
*/
public function setPresenter(Presenter $presenter)
{
$this->presenter = $presenter;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
list($grep, $insensitive, $invert) = FilterOptions::getOptions();
$this
->setName('ls')
->setAliases(['list', 'dir'])
->setDefinition([
new CodeArgument('target', CodeArgument::OPTIONAL, 'A target class or object to list.'),
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>
<return>>>> ls -l --properties new DateTime()</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);
} 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 = [
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([$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([]);
foreach ($items as $item) {
$table->addRow([$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 (['properties', 'methods', 'no-inherit'] as $option) {
if ($input->getOption($option)) {
throw new RuntimeException('--' . $option . ' does not make sense without a specified target');
}
}
foreach (['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 (['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 (['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,127 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\ReflectionClassConstant;
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 = [];
$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 = [];
foreach ($reflector->getConstants() as $name => $constant) {
$constReflector = ReflectionClassConstant::create($reflector->name, $name);
if ($noInherit && $constReflector->getDeclaringClass()->getName() !== $className) {
continue;
}
$constants[$name] = $constReflector;
}
\ksort($constants, SORT_NATURAL | SORT_FLAG_CASE);
return $constants;
}
/**
* Prepare formatted constant array.
*
* @param array $constants
*
* @return array
*/
protected function prepareConstants(array $constants)
{
// My kingdom for a generator.
$ret = [];
foreach ($constants as $name => $constant) {
if ($this->showItem($name)) {
$ret[$name] = [
'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-2018 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 = [];
// 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 ($input->getOption('traits')) {
$ret = \array_merge($ret, $this->filterClasses('Traits', \get_declared_traits(), $internal, $user));
}
return \array_map([$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 = [];
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 $classes
*
* @return array
*/
protected function prepareClasses(array $classes)
{
\natcasesort($classes);
// My kingdom for a generator.
$ret = [];
foreach ($classes as $name) {
if ($this->showItem($name)) {
$ret[$name] = [
'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-2018 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 = [];
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([$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] : [];
}
/**
* Prepare formatted constant array.
*
* @param array $constants
*
* @return array
*/
protected function prepareConstants(array $constants)
{
// My kingdom for a generator.
$ret = [];
$names = \array_keys($constants);
\natcasesort($names);
foreach ($names as $name) {
if ($this->showItem($name)) {
$ret[$name] = [
'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-2018 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-2018 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 = [];
$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 = [];
foreach ($functions as $name) {
if ($this->showItem($name)) {
$ret[$name] = [
'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-2018 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 [
'Global Variables' => $globals,
];
}
/**
* Get defined global variables.
*
* @return array
*/
protected function getGlobals()
{
global $GLOBALS;
$names = \array_keys($GLOBALS);
\natcasesort($names);
$ret = [];
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 = [];
foreach ($globals as $name => $value) {
if ($this->showItem($name)) {
$fname = '$' . $name;
$ret[$fname] = [
'name' => $fname,
'style' => self::IS_GLOBAL,
'value' => $this->presentRef($value),
];
}
}
return $ret;
}
}

View file

@ -0,0 +1,89 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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);
parent::__construct($presenter);
}
/**
* {@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 [
'Interfaces' => $interfaces,
];
}
/**
* Prepare formatted interface array.
*
* @param array $interfaces
*
* @return array
*/
protected function prepareInterfaces(array $interfaces)
{
\natcasesort($interfaces);
// My kingdom for a generator.
$ret = [];
foreach ($interfaces as $name) {
if ($this->showItem($name)) {
$ret[$name] = [
'name' => $name,
'style' => self::IS_CLASS,
'value' => $this->presentSignature($name),
];
}
}
return $ret;
}
}

View file

@ -0,0 +1,145 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 = [];
$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 = [];
foreach ($reflector->getMethods() as $name => $method) {
if ($noInherit && $method->getDeclaringClass()->getName() !== $className) {
continue;
}
if ($showAll || $method->isPublic()) {
$methods[$method->getName()] = $method;
}
}
\ksort($methods, SORT_NATURAL | SORT_FLAG_CASE);
return $methods;
}
/**
* Prepare formatted method array.
*
* @param array $methods
*
* @return array
*/
protected function prepareMethods(array $methods)
{
// My kingdom for a generator.
$ret = [];
foreach ($methods as $name => $method) {
if ($this->showItem($name)) {
$ret[$name] = [
'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,180 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 = [];
$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 = [];
foreach ($reflector->getProperties() as $property) {
if ($noInherit && $property->getDeclaringClass()->getName() !== $className) {
continue;
}
if ($showAll || $property->isPublic()) {
$properties[$property->getName()] = $property;
}
}
\ksort($properties, SORT_NATURAL | SORT_FLAG_CASE);
return $properties;
}
/**
* Prepare formatted property array.
*
* @param array $properties
*
* @return array
*/
protected function prepareProperties(array $properties, $target = null)
{
// My kingdom for a generator.
$ret = [];
foreach ($properties as $name => $property) {
if ($this->showItem($name)) {
$fname = '$' . $name;
$ret[$fname] = [
'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,89 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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);
parent::__construct($presenter);
}
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
{
// 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 [
'Traits' => $traits,
];
}
/**
* Prepare formatted trait array.
*
* @param array $traits
*
* @return array
*/
protected function prepareTraits(array $traits)
{
\natcasesort($traits);
// My kingdom for a generator.
$ret = [];
foreach ($traits as $name) {
if ($this->showItem($name)) {
$ret[$name] = [
'name' => $name,
'style' => self::IS_CLASS,
'value' => $this->presentSignature($name),
];
}
}
return $ret;
}
}

View file

@ -0,0 +1,137 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 = [
'_', '_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 [
'Variables' => $variables,
];
}
/**
* Get scope variables.
*
* @param bool $showAll Include special variables (e.g. $_)
*
* @return array
*/
protected function getVariables($showAll)
{
$scopeVars = $this->context->getAll();
\uksort($scopeVars, function ($a, $b) {
$aIndex = \array_search($a, self::$specialNames);
$bIndex = \array_search($b, self::$specialNames);
if ($aIndex !== false) {
if ($bIndex !== false) {
return $aIndex - $bIndex;
}
return 1;
}
if ($bIndex !== false) {
return -1;
}
return \strnatcasecmp($a, $b);
});
$ret = [];
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 = [];
foreach ($variables as $name => $val) {
if ($this->showItem($name)) {
$fname = '$' . $name;
$ret[$fname] = [
'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,180 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Context;
use Psy\ContextAware;
use Psy\Input\CodeArgument;
use Psy\ParserFactory;
use Psy\VarDumper\Presenter;
use Psy\VarDumper\PresenterAware;
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 ContextAware, PresenterAware
{
/**
* Context instance (for ContextAware interface).
*
* @var Context
*/
protected $context;
private $presenter;
private $parserFactory;
private $parsers;
/**
* {@inheritdoc}
*/
public function __construct($name = null)
{
$this->parserFactory = new ParserFactory();
$this->parsers = [];
parent::__construct($name);
}
/**
* ContextAware interface.
*
* @param Context $context
*/
public function setContext(Context $context)
{
$this->context = $context;
}
/**
* PresenterAware interface.
*
* @param Presenter $presenter
*/
public function setPresenter(Presenter $presenter)
{
$this->presenter = clone $presenter;
$this->presenter->addCasters([
'PhpParser\Node' => function (Node $node, array $a) {
$a = [
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 = [
new CodeArgument('code', CodeArgument::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));
$this->context->setReturnValue($nodes);
}
/**
* 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-2018 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([])
->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,303 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\CodeCleaner\NoReturnValue;
use Psy\Context;
use Psy\ContextAware;
use Psy\Exception\ErrorException;
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 CLASS_MEMBER = '/^([\\\\\w]+)::(\w+)$/';
const CLASS_STATIC = '/^([\\\\\w]+)::\$(\w+)$/';
const INSTANCE_MEMBER = '/^(\$\w+)(::|->)(\w+)$/';
/**
* 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
*
* @return array (class or instance name, member name, kind)
*/
protected function getTarget($valueName)
{
$valueName = \trim($valueName);
$matches = [];
switch (true) {
case \preg_match(self::CLASS_OR_FUNC, $valueName, $matches):
return [$this->resolveName($matches[0], true), null, 0];
case \preg_match(self::CLASS_MEMBER, $valueName, $matches):
return [$this->resolveName($matches[1]), $matches[2], Mirror::CONSTANT | Mirror::METHOD];
case \preg_match(self::CLASS_STATIC, $valueName, $matches):
return [$this->resolveName($matches[1]), $matches[2], Mirror::STATIC_PROPERTY | Mirror::PROPERTY];
case \preg_match(self::INSTANCE_MEMBER, $valueName, $matches):
if ($matches[2] === '->') {
$kind = Mirror::METHOD | Mirror::PROPERTY;
} else {
$kind = Mirror::CONSTANT | Mirror::METHOD;
}
return [$this->resolveObject($matches[1]), $matches[3], $kind];
default:
return [$this->resolveObject($valueName), null, 0];
}
}
/**
* Resolve a class or function name (with the current shell namespace).
*
* @throws ErrorException when `self` or `static` is used in a non-class scope
*
* @param string $name
* @param bool $includeFunctions (default: false)
*
* @return string
*/
protected function resolveName($name, $includeFunctions = false)
{
$shell = $this->getApplication();
// While not *technically* 100% accurate, let's treat `self` and `static` as equivalent.
if (\in_array(\strtolower($name), ['self', 'static'])) {
if ($boundClass = $shell->getBoundClass()) {
return $boundClass;
}
if ($boundObject = $shell->getBoundObject()) {
return \get_class($boundObject);
}
$msg = \sprintf('Cannot use "%s" when no class scope is active', \strtolower($name));
throw new ErrorException($msg, 0, E_USER_ERROR, "eval()'d code", 1);
}
if (\substr($name, 0, 1) === '\\') {
return $name;
}
if ($namespace = $shell->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
*
* @return array (value, Reflector)
*/
protected function getTargetAndReflector($valueName)
{
list($value, $member, $kind) = $this->getTarget($valueName);
return [$value, Mirror::get($value, $member, $kind)];
}
/**
* Resolve code to a value in the current scope.
*
* @throws RuntimeException when the code does not return a value in the current scope
*
* @param string $code
*
* @return mixed Variable value
*/
protected function resolveCode($code)
{
try {
$value = $this->getApplication()->execute($code, true);
} catch (\Exception $e) {
// Swallow all exceptions?
}
if (!isset($value) || $value instanceof NoReturnValue) {
throw new RuntimeException('Unknown target: ' . $code);
}
return $value;
}
/**
* Resolve code to an object in the current scope.
*
* @throws RuntimeException when the code resolves to a non-object value
*
* @param string $code
*
* @return object Variable instance
*/
private function resolveObject($code)
{
$value = $this->resolveCode($code);
if (!\is_object($value)) {
throw new RuntimeException('Unable to inspect a non-object');
}
return $value;
}
/**
* @deprecated Use `resolveCode` instead
*
* @param string $name
*
* @return mixed Variable instance
*/
protected function resolveInstance($name)
{
@\trigger_error('`resolveInstance` is deprecated; use `resolveCode` instead.', E_USER_DEPRECATED);
return $this->resolveCode($name);
}
/**
* 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 = [];
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 'ReflectionClassConstant':
case 'Psy\Reflection\ReflectionClassConstant':
$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;
case 'Psy\Reflection\ReflectionConstant_':
if ($reflector->inNamespace()) {
$vars['__namespace'] = $reflector->getNamespaceName();
}
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,289 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Input\CodeArgument;
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 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;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('show')
->setDefinition([
new CodeArgument('target', CodeArgument::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('target')) {
throw new \InvalidArgumentException('Too many arguments (supply either "target" or "--ex")');
}
return $this->writeExceptionContext($input, $output);
}
if ($input->getArgument('target')) {
return $this->writeCodeContext($input, $output);
}
throw new RuntimeException('Not enough arguments (missing: "target")');
}
private function writeCodeContext(InputInterface $input, OutputInterface $output)
{
list($target, $reflector) = $this->getTargetAndReflector($input->getArgument('target'));
// 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, [
'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 = [];
if (isset($context['class'])) {
$vars['__class'] = $context['class'];
if (isset($context['function'])) {
$vars['__method'] = $context['function'];
}
try {
$refl = new \ReflectionClass($context['class']);
if ($namespace = $refl->getNamespaceName()) {
$vars['__namespace'] = $namespace;
}
} catch (\Exception $e) {
// oh well
}
} elseif (isset($context['function'])) {
$vars['__function'] = $context['function'];
try {
$refl = new \ReflectionFunction($context['function']);
if ($namespace = $refl->getNamespaceName()) {
$vars['__namespace'] = $namespace;
}
} catch (\Exception $e) {
// oh well
}
}
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 [$matches[1], $matches[2]];
}
}
}

View file

@ -0,0 +1,143 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\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([
new CodeArgument('code', CodeArgument::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);
$shell = $this->getApplication();
$shell->addCode($sudoCode, !$shell->hasCode());
}
/**
* 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,172 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\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;
use Psy\ContextAware;
use Psy\Input\CodeArgument;
use Psy\ParserFactory;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Throw an exception or error out of the Psy Shell.
*/
class ThrowUpCommand extends Command implements ContextAware
{
const THROW_CLASS = 'Psy\Exception\ThrowUpException';
private $parser;
private $printer;
/**
* Context instance (for ContextAware interface).
*
* @var Context
*/
protected $context;
/**
* {@inheritdoc}
*/
public function __construct($name = null)
{
$parserFactory = new ParserFactory();
$this->parser = $parserFactory->createParser();
$this->printer = new Printer();
parent::__construct($name);
}
/**
* ContextAware interface.
*
* @param Context $context
*/
public function setContext(Context $context)
{
$this->context = $context;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('throw-up')
->setDefinition([
new CodeArgument('exception', CodeArgument::OPTIONAL, 'Exception or Error to throw.'),
])
->setDescription('Throw an exception or error out of the Psy Shell.')
->setHelp(
<<<'HELP'
Throws an exception or error 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>
<return>>>> throw-up new Exception('WHEEEEEE!')</return>
<return>>>> throw-up "bye!"</return>
HELP
);
}
/**
* {@inheritdoc}
*
* @throws InvalidArgumentException if there is no exception to throw
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$args = $this->prepareArgs($input->getArgument('exception'));
$throwStmt = new Throw_(new StaticCall(new FullyQualifiedName(self::THROW_CLASS), 'fromThrowable', $args));
$throwCode = $this->printer->prettyPrint([$throwStmt]);
$shell = $this->getApplication();
$shell->addCode($throwCode, !$shell->hasCode());
}
/**
* Parse the supplied command argument.
*
* If no argument was given, this falls back to `$_e`
*
* @throws InvalidArgumentException if there is no exception to throw
*
* @param string $code
*
* @return Arg[]
*/
private function prepareArgs($code = null)
{
if (!$code) {
// Default to last exception if nothing else was supplied
return [new Arg(new Variable('_e'))];
}
if (\strpos('<?', $code) === false) {
$code = '<?php ' . $code;
}
$nodes = $this->parse($code);
if (\count($nodes) !== 1) {
throw new \InvalidArgumentException('No idea how to throw this');
}
$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;
}
/**
* 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,195 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Command\TimeitCommand\TimeitVisitor;
use Psy\Input\CodeArgument;
use Psy\ParserFactory;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class TimeitCommand.
*/
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}
*/
protected function configure()
{
$this
->setName('timeit')
->setDefinition([
new InputOption('num', 'n', InputOption::VALUE_REQUIRED, 'Number of iterations.'),
new CodeArgument('code', CodeArgument::REQUIRED, 'Code to execute.'),
])
->setDescription('Profiles with a timer.')
->setHelp(
<<<'HELP'
Time profiling for functions and commands.
e.g.
<return>>>> timeit sleep(1)</return>
<return>>>> timeit -n1000 $closure()</return>
HELP
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$code = $input->getArgument('code');
$num = $input->getOption('num') ?: 1;
$shell = $this->getApplication();
$instrumentedCode = $this->instrumentCode($code);
self::$times = [];
for ($i = 0; $i < $num; $i++) {
$_ = $shell->execute($instrumentedCode);
$this->ensureEndMarked();
}
$shell->writeReturnValue($_);
$times = self::$times;
self::$times = [];
if ($num === 1) {
$output->writeln(\sprintf(self::RESULT_MSG, $times[0]));
} else {
$total = \array_sum($times);
\rsort($times);
$median = $times[\round($num / 2)];
$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 . ';');
}
}
}

View file

@ -0,0 +1,139 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Command\TimeitCommand;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Return_;
use PhpParser\NodeVisitorAbstract;
use Psy\CodeCleaner\NoReturnValue;
/**
* A node visitor for instrumenting code to be executed by the `timeit` command.
*
* Injects `TimeitCommand::markStart()` at the start of code to be executed, and
* `TimeitCommand::markEnd()` at the end, and on top-level return statements.
*/
class TimeitVisitor extends NodeVisitorAbstract
{
private $functionDepth;
/**
* {@inheritdoc}
*/
public function beforeTraverse(array $nodes)
{
$this->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, $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 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[] = $this->maybeExpression($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)]);
}
/**
* 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;
}
}

View file

@ -0,0 +1,167 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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([
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 = [];
$trace = $e->getTrace();
\array_unshift($trace, [
'function' => '',
'file' => $e->getFile() !== null ? $e->getFile() : 'n/a',
'line' => $e->getLine() !== null ? $e->getLine() : 'n/a',
'args' => [],
]);
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("#/src/Execution(?:Loop)?Closure.php\(\d+\) : eval\(\)'d code$#", \str_replace('\\', '/', $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,148 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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;
private $backtrace;
/**
* @param null|string $colorMode (default: null)
*/
public function __construct($colorMode = null)
{
$this->colorMode = $colorMode ?: Configuration::COLOR_MODE_AUTO;
$this->backtrace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('whereami')
->setDefinition([
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, ['__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-2018 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(['last-exception', 'wtf?'])
->setDefinition([
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 -a</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();
}
}

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

@ -0,0 +1,237 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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([$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() : [$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() : [$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(['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 = [];
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;
}
}

1307
vendor/psy/psysh/src/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-2018 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, ['blue']);
$color->addTheme(Highlighter::TOKEN_KEYWORD, ['yellow']);
$color->addTheme(Highlighter::TOKEN_STRING, ['green']);
$color->addTheme(Highlighter::TOKEN_COMMENT, ['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, ['none']);
$color->addTheme(Highlighter::TOKEN_COMMENT, ['none']);
$color->addTheme(Highlighter::TOKEN_KEYWORD, ['none']);
$color->addTheme(Highlighter::TOKEN_DEFAULT, ['none']);
$color->addTheme(Highlighter::TOKEN_HTML, ['none']);
$color->addTheme(Highlighter::ACTUAL_LINE_MARK, ['none']);
$color->addTheme(Highlighter::LINE_NUMBER, ['none']);
return $color;
}
}

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

@ -0,0 +1,320 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 = ['_', '_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 = [
'__function', '__method', '__class', '__namespace', '__file', '__line', '__dir',
];
private $scopeVariables = [];
private $commandScopeVariables = [];
private $returnValue;
private $lastException;
private $lastStdout;
private $boundObject;
private $boundClass;
/**
* 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 = [
'_' => $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.
*
* Note that this unsets the bound class, if any exists.
*
* @param object|null $boundObject
*/
public function setBoundObject($boundObject)
{
$this->boundObject = \is_object($boundObject) ? $boundObject : null;
$this->boundClass = null;
}
/**
* Get the bound object ($this variable) for the interactive shell.
*
* @return object|null
*/
public function getBoundObject()
{
return $this->boundObject;
}
/**
* Set the bound class (self) for the interactive shell.
*
* Note that this unsets the bound object, if any exists.
*
* @param string|null $boundClass
*/
public function setBoundClass($boundClass)
{
$this->boundClass = (\is_string($boundClass) && $boundClass !== '') ? $boundClass : null;
$this->boundObject = null;
}
/**
* Get the bound class (self) for the interactive shell.
*
* @return string|null
*/
public function getBoundClass()
{
return $this->boundClass;
}
/**
* Set command-scope magic variables: $__class, $__file, etc.
*
* @param array $commandScopeVariables
*/
public function setCommandScopeVariables(array $commandScopeVariables)
{
$vars = [];
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);
}
}

28
vendor/psy/psysh/src/ContextAware.php vendored Normal file
View file

@ -0,0 +1,28 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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-2018 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-2018 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-2018 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-2018 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-2018 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-2018 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-2018 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,57 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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();
}
/**
* Create a ThrowUpException from a Throwable.
*
* @param \Throwable $throwable
*
* @return ThrowUpException
*/
public static function fromThrowable($throwable)
{
if ($throwable instanceof \Error) {
$throwable = ErrorException::fromError($throwable);
}
if (!$throwable instanceof \Exception) {
throw new \InvalidArgumentException('throw-up can only throw Exceptions and Errors');
}
return new self($throwable);
}
}

View file

@ -0,0 +1,55 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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->getCode());
}
}

View file

@ -0,0 +1,119 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy;
/**
* The Psy Shell's execution scope.
*/
class ExecutionClosure
{
const NOOP_INPUT = 'return null;';
private $closure;
/**
* @param Shell $__psysh__
*/
public function __construct(Shell $__psysh__)
{
$this->setClosure($__psysh__, function () use ($__psysh__) {
try {
// Restore execution scope variables
\extract($__psysh__->getScopeVariables(false));
// Buffer stdout; we'll need it later
\ob_start([$__psysh__, 'writeStdout'], 1);
// Convert all errors to exceptions
\set_error_handler([$__psysh__, 'handleError']);
// Evaluate the current code buffer
$_ = eval($__psysh__->onExecute($__psysh__->flushCode() ?: ExecutionClosure::NOOP_INPUT));
} catch (\Throwable $_e) {
// Clean up on our way out.
\restore_error_handler();
if (\ob_get_level() > 0) {
\ob_end_clean();
}
throw $_e;
} catch (\Exception $_e) {
// Clean up on our way out.
\restore_error_handler();
if (\ob_get_level() > 0) {
\ob_end_clean();
}
throw $_e;
}
// Won't be needing this anymore
\restore_error_handler();
// Flush stdout (write to shell output, plus save to magic variable)
\ob_end_flush();
// Save execution scope variables for next time
$__psysh__->setScopeVariables(\get_defined_vars());
return $_;
});
}
/**
* Set the closure instance.
*
* @param Shell $psysh
* @param \Closure $closure
*/
protected function setClosure(Shell $shell, \Closure $closure)
{
if (self::shouldBindClosure()) {
$that = $shell->getBoundObject();
if (\is_object($that)) {
$closure = $closure->bindTo($that, \get_class($that));
} else {
$closure = $closure->bindTo(null, $shell->getBoundClass());
}
}
$this->closure = $closure;
}
/**
* Go go gadget closure.
*
* @return mixed
*/
public function execute()
{
$closure = $this->closure;
return $closure();
}
/**
* Decide whether to bind the execution closure.
*
* @return bool
*/
protected static function shouldBindClosure()
{
// 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 true;
}
}

67
vendor/psy/psysh/src/ExecutionLoop.php vendored Normal file
View file

@ -0,0 +1,67 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy;
use Psy\Exception\ErrorException;
/**
* The Psy Shell execution loop.
*/
class ExecutionLoop
{
/**
* Run the execution loop.
*
* @throws ThrowUpException if thrown by the `throw-up` command
*
* @param Shell $shell
*/
public function run(Shell $shell)
{
$this->loadIncludes($shell);
$closure = new ExecutionLoopClosure($shell);
$closure->execute();
}
/**
* Load user-defined includes.
*
* @param Shell $shell
*/
protected function loadIncludes(Shell $shell)
{
// Load user-defined includes
$load = function (Shell $__psysh__) {
\set_error_handler([$__psysh__, 'handleError']);
foreach ($__psysh__->getIncludes() as $__psysh_include__) {
try {
include $__psysh_include__;
} catch (\Error $_e) {
$__psysh__->writeException(ErrorException::fromError($_e));
} catch (\Exception $_e) {
$__psysh__->writeException($_e);
}
}
\restore_error_handler();
unset($__psysh_include__);
// Override any new local variables with pre-defined scope variables
\extract($__psysh__->getScopeVariables(false));
// ... then add the whole mess of variables back.
$__psysh__->setScopeVariables(\get_defined_vars());
};
$load($shell);
}
}

View file

@ -0,0 +1,62 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Shell;
/**
* Abstract Execution Loop Listener class.
*/
abstract class AbstractListener implements Listener
{
/**
* {@inheritdoc}
*/
public function beforeRun(Shell $shell)
{
}
/**
* {@inheritdoc}
*/
public function beforeLoop(Shell $shell)
{
}
/**
* {@inheritdoc}
*/
public function onInput(Shell $shell, $input)
{
}
/**
* {@inheritdoc}
*/
public function onExecute(Shell $shell, $code)
{
}
/**
* {@inheritdoc}
*/
public function afterLoop(Shell $shell)
{
}
/**
* {@inheritdoc}
*/
public function afterRun(Shell $shell)
{
}
}

View file

@ -0,0 +1,83 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Shell;
/**
* Execution Loop Listener interface.
*/
interface Listener
{
/**
* Determines whether this listener should be active.
*
* @return bool
*/
public static function isSupported();
/**
* Called once before the REPL session starts.
*
* @param Shell $shell
*/
public function beforeRun(Shell $shell);
/**
* Called at the start of each loop.
*
* @param Shell $shell
*/
public function beforeLoop(Shell $shell);
/**
* Called on user input.
*
* Return a new string to override or rewrite user input.
*
* @param Shell $shell
* @param string $input
*
* @return string|null User input override
*/
public function onInput(Shell $shell, $input);
/**
* Called before executing user code.
*
* Return a new string to override or rewrite user code.
*
* Note that this is run *after* the Code Cleaner, so if you return invalid
* or unsafe PHP here, it'll be executed without any of the safety Code
* Cleaner provides. This comes with the big kid warranty :)
*
* @param Shell $shell
* @param string $code
*
* @return string|null User code override
*/
public function onExecute(Shell $shell, $code);
/**
* Called at the end of each loop.
*
* @param Shell $shell
*/
public function afterLoop(Shell $shell);
/**
* Called once after the REPL session ends.
*
* @param Shell $shell
*/
public function afterRun(Shell $shell);
}

View file

@ -0,0 +1,219 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Exception\BreakException;
use Psy\Shell;
/**
* An execution loop listener that forks the process before executing code.
*
* This is awesome, as the session won't die prematurely if user input includes
* a fatal error, such as redeclaring a class or function.
*/
class ProcessForker extends AbstractListener
{
private $savegame;
private $up;
/**
* Process forker is supported if pcntl and posix extensions are available.
*
* @return bool
*/
public static function isSupported()
{
return \function_exists('pcntl_signal') && \function_exists('posix_getpid');
}
/**
* 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 beforeRun(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 = [$down];
$write = null;
$except = null;
do {
$n = @\stream_select($read, $write, $except, null);
if ($n === 0) {
throw new \RuntimeException('Process timed out waiting for execution loop');
}
if ($n === false) {
$err = \error_get_last();
if (!isset($err['message']) || \stripos($err['message'], 'interrupted system call') === false) {
$msg = $err['message'] ?
\sprintf('Error waiting for execution loop: %s', $err['message']) :
'Error waiting for execution loop';
throw new \RuntimeException($msg);
}
}
} while ($n < 1);
$content = \stream_get_contents($down);
\fclose($down);
if ($content) {
$shell->setScopeVariables(@\unserialize($content));
}
throw new BreakException('Exiting main thread');
}
// 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);
// Save this; we'll need to close it in `afterRun`
$this->up = $up;
}
/**
* Create a savegame at the start of each loop iteration.
*
* @param Shell $shell
*/
public function beforeLoop(Shell $shell)
{
$this->createSavegame();
}
/**
* Clean up old savegames at the end of each loop iteration.
*
* @param Shell $shell
*/
public function afterLoop(Shell $shell)
{
// if there's an old savegame hanging around, let's kill it.
if (isset($this->savegame)) {
\posix_kill($this->savegame, SIGKILL);
\pcntl_signal_dispatch();
}
}
/**
* After the REPL session ends, send the scope variables back up to the main
* thread (if this is a child thread).
*
* @param Shell $shell
*/
public function afterRun(Shell $shell)
{
// We're a child thread. Send the scope variables back up to the main thread.
if (isset($this->up)) {
\fwrite($this->up, $this->serializeReturn($shell->getScopeVariables(false)));
\fclose($this->up);
\posix_kill(\posix_getpid(), SIGKILL);
}
}
/**
* 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 = [];
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 (\Throwable $e) {
// we'll just ignore this one...
} catch (\Exception $e) {
// and this one too...
// @todo remove this once we don't support PHP 5.x anymore :)
}
}
return @\serialize($serializable);
}
}

View file

@ -0,0 +1,135 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Exception\ParseErrorException;
use Psy\ParserFactory;
use Psy\Shell;
/**
* A runkit-based code reloader, which is pretty much magic.
*/
class RunkitReloader extends AbstractListener
{
private $parser;
private $timestamps = [];
/**
* Only enabled if Runkit is installed.
*
* @return bool
*/
public static function isSupported()
{
return \extension_loaded('runkit');
}
/**
* Construct a Runkit Reloader.
*
* @todo Pass in Parser Factory instance for dependency injection?
*/
public function __construct()
{
$parserFactory = new ParserFactory();
$this->parser = $parserFactory->createParser();
}
/**
* Reload code on input.
*
* @param Shell $shell
* @param string $input
*/
public function onInput(Shell $shell, $input)
{
$this->reload($shell);
}
/**
* Look through included files and update anything with a new timestamp.
*
* @param Shell $shell
*/
private function reload(Shell $shell)
{
\clearstatcache();
$modified = [];
foreach (\get_included_files() as $file) {
$timestamp = \filemtime($file);
if (!isset($this->timestamps[$file])) {
$this->timestamps[$file] = $timestamp;
continue;
}
if ($this->timestamps[$file] === $timestamp) {
continue;
}
if (!$this->lintFile($file)) {
$msg = \sprintf('Modified file "%s" could not be reloaded', $file);
$shell->writeException(new ParseErrorException($msg));
continue;
}
$modified[] = $file;
$this->timestamps[$file] = $timestamp;
}
// switch (count($modified)) {
// case 0:
// return;
// case 1:
// printf("Reloading modified file: \"%s\"\n", str_replace(getcwd(), '.', $file));
// break;
// default:
// printf("Reloading %d modified files\n", count($modified));
// break;
// }
foreach ($modified as $file) {
runkit_import($file, (
RUNKIT_IMPORT_FUNCTIONS |
RUNKIT_IMPORT_CLASSES |
RUNKIT_IMPORT_CLASS_METHODS |
RUNKIT_IMPORT_CLASS_CONSTS |
RUNKIT_IMPORT_CLASS_PROPS |
RUNKIT_IMPORT_OVERRIDE
));
}
}
/**
* Should this file be re-imported?
*
* Use PHP-Parser to ensure that the file is valid PHP.
*
* @param string $file
*
* @return bool
*/
private function lintFile($file)
{
// first try to parse it
try {
$this->parser->parse(\file_get_contents($file));
} catch (\Exception $e) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,104 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy;
use Psy\Exception\BreakException;
use Psy\Exception\ErrorException;
use Psy\Exception\ThrowUpException;
use Psy\Exception\TypeErrorException;
/**
* The Psy Shell's execution loop scope.
*
* @todo Once we're on PHP 5.5, we can switch ExecutionClosure to a generator
* and get rid of the duplicate closure implementations :)
*/
class ExecutionLoopClosure extends ExecutionClosure
{
/**
* @param Shell $__psysh__
*/
public function __construct(Shell $__psysh__)
{
$this->setClosure($__psysh__, function () use ($__psysh__) {
// Restore execution scope variables
\extract($__psysh__->getScopeVariables(false));
do {
$__psysh__->beforeLoop();
try {
$__psysh__->getInput();
try {
// Pull in any new execution scope variables
if ($__psysh__->getLastExecSuccess()) {
\extract($__psysh__->getScopeVariablesDiff(\get_defined_vars()));
}
// Buffer stdout; we'll need it later
\ob_start([$__psysh__, 'writeStdout'], 1);
// Convert all errors to exceptions
\set_error_handler([$__psysh__, 'handleError']);
// Evaluate the current code buffer
$_ = eval($__psysh__->onExecute($__psysh__->flushCode() ?: ExecutionClosure::NOOP_INPUT));
} catch (\Throwable $_e) {
// Clean up on our way out.
\restore_error_handler();
if (\ob_get_level() > 0) {
\ob_end_clean();
}
throw $_e;
} catch (\Exception $_e) {
// Clean up on our way out.
\restore_error_handler();
if (\ob_get_level() > 0) {
\ob_end_clean();
}
throw $_e;
}
// Won't be needing this anymore
\restore_error_handler();
// Flush stdout (write to shell output, plus save to magic variable)
\ob_end_flush();
// Save execution scope variables for next time
$__psysh__->setScopeVariables(\get_defined_vars());
$__psysh__->writeReturnValue($_);
} catch (BreakException $_e) {
$__psysh__->writeException($_e);
return;
} catch (ThrowUpException $_e) {
$__psysh__->writeException($_e);
throw $_e;
} catch (\TypeError $_e) {
$__psysh__->writeException(TypeErrorException::fromTypeError($_e));
} catch (\Error $_e) {
$__psysh__->writeException(ErrorException::fromError($_e));
} catch (\Exception $_e) {
$__psysh__->writeException($_e);
}
$__psysh__->afterLoop();
} while (true);
});
}
}

View file

@ -0,0 +1,71 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 ($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;
}
}

View file

@ -0,0 +1,168 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 = [
'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 = [];
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 = [' '];
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(['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 = [];
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-2018 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,308 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\ReflectionClassConstant;
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 ReflectionClassConstant:
case $reflector instanceof \ReflectionClassConstant:
return self::formatClassConstant($reflector);
case $reflector instanceof \ReflectionMethod:
return self::formatMethod($reflector);
case $reflector instanceof \ReflectionProperty:
return self::formatProperty($reflector);
case $reflector instanceof ReflectionConstant_:
return self::formatConstant($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.
*
* @param \Reflector $reflector
*
* @return string Formatted modifiers
*/
private static function formatModifiers(\Reflector $reflector)
{
if ($reflector instanceof \ReflectionClass && $reflector->isTrait()) {
// For some reason, PHP 5.x returns `abstract public` modifiers for
// traits. Let's just ignore that business entirely.
if (\version_compare(PHP_VERSION, '7.0.0', '<')) {
return [];
}
}
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 = [];
if ($modifiers = self::formatModifiers($reflector)) {
$chunks[] = $modifiers;
}
if ($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 ReflectionClassConstant|\ReflectionClassConstant $reflector
*
* @return string Formatted signature
*/
private static function formatClassConstant($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
);
}
/**
* Format a constant signature.
*
* @param ReflectionConstant_ $reflector
*
* @return string Formatted signature
*/
private static function formatConstant($reflector)
{
$value = $reflector->getValue();
$style = self::getTypeStyle($value);
return \sprintf(
'<keyword>define</keyword>(<string>%s</string>, <%s>%s</%s>)',
OutputFormatter::escape(Json::encode($reflector->getName())),
$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'; // @codeCoverageIgnore
}
}
/**
* 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 array
*/
private static function formatFunctionParams(\ReflectionFunctionAbstract $reflector)
{
$params = [];
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
// @codeCoverageIgnoreStart
$chunks = \explode('$' . $param->getName(), (string) $param);
$chunks = \explode(' ', \trim($chunks[0]));
$guess = \end($chunks);
$hint = \sprintf('<urgent>%s</urgent> ', $guess);
// @codeCoverageIgnoreEnd
}
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;
}
}

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