Upgrade framework

This commit is contained in:
2023-11-14 16:54:35 +01:00
parent 1648a5cd42
commit 4fcf6fffcc
10548 changed files with 693138 additions and 466698 deletions

View File

@@ -1,12 +0,0 @@
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

View File

@@ -1,9 +0,0 @@
build-vendor/
vendor/
composer.lock
composer-compat.json
composer-compat.lock
manual/
dist/
__pycache__
.php_cs.cache

View File

@@ -1,46 +0,0 @@
<?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/'
],
];

View File

@@ -1,44 +0,0 @@
<?php
use Symfony\CS\Config\Config;
use Symfony\CS\FixerInterface;
use Symfony\CS\Fixer\Contrib\HeaderCommentFixer;
$header = <<<EOF
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.
EOF;
HeaderCommentFixer::setHeader($header);
$config = Config::create()
// use symfony level and extra fixers:
->level(FixerInterface::SYMFONY_LEVEL)
->fixers(array(
'align_double_arrow',
'concat_with_spaces',
'header_comment',
'long_array_syntax',
'ordered_use',
'strict',
'-concat_without_spaces',
'-method_argument_space',
'-pre_increment',
'-unalign_double_arrow',
'-unalign_equals',
'-no_empty_comment', // stop removing slashes in the middle of multi-line comments
))
->setUsingLinter(false);
$finder = $config->getFinder()
->in(__DIR__)
->name('.php_cs')
->name('build-manual')
->name('build-phar')
->exclude('build-vendor');
return $config;

View File

@@ -1,22 +0,0 @@
preset: symfony
enabled:
- align_double_arrow
- concat_with_spaces
- long_array_syntax
- ordered_use
- strict
disabled:
- concat_without_spaces
- method_argument_space
- pre_increment
- unalign_double_arrow
- unalign_equals
finder:
name:
- "*.php"
- ".php_cs"
- "build-manual"
- "build-phar"

View File

@@ -1,34 +0,0 @@
language: php
sudo: false
matrix:
include:
- php: 5.3
- php: 5.4
- php: 5.5
- php: 5.6
- php: 7.0
- php: 7.1
- php: hhvm
dist: trusty
allow_failures:
- php: hhvm
install: travis_retry composer update --no-interaction
script: vendor/bin/phpunit --verbose
before_deploy: bin/package -v $TRAVIS_TAG
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 = 5.3* || $TRAVIS_PHP_VERSION = 7.1*)

View File

@@ -1,18 +0,0 @@
## 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 run `vendor/bin/php-cs-fixer fix` before committing.
## Branching model
Please branch off and send pull requests to the `develop` branch.
## Building the manual
```sh
svn co https://svn.php.net/repository/phpdoc/en/trunk/reference/ php_manual
bin/build_manual phpdoc_manual ~/.local/share/psysh/php_manual.sqlite
```
To build the manual for another language, switch out `en` above for `de`, `es`, or any of the other languages listed in the docs.
[Partial or outdated documentation is available for other languages](http://www.php.net/manual/help-translate.php) but these translations are outdated, so their content may be completely wrong or insecure!

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2012-2017 Justin Hileman
Copyright (c) 2012-2022 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

View File

@@ -7,7 +7,7 @@ PsySH is a runtime developer console, interactive debugger and [REPL](https://en
[![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)
[![Build status](https://img.shields.io/github/actions/workflow/status/bobthecow/psysh/tests.yml?branch=main&style=flat-square)](https://github.com/bobthecow/psysh/actions?query=branch:main)
[![StyleCI](https://styleci.io/repos/4549925/shield)](https://styleci.io/repos/4549925)
@@ -17,17 +17,20 @@ PsySH is a runtime developer console, interactive debugger and [REPL](https://en
### [💾 Installation](https://github.com/bobthecow/psysh/wiki/Installation)
* [📕 PHP manual installation](https://github.com/bobthecow/psysh/wiki/PHP-manual)
* <a class="internal present" href="https://github.com/bobthecow/psysh/wiki/Windows"><img src="https://user-images.githubusercontent.com/53660/40878809-407e8368-664b-11e8-8455-f11602c41dfe.png" width="18"> Windows</a>
### [🖥 Usage](https://github.com/bobthecow/psysh/wiki/Usage)
* [✨ Magic variables](https://github.com/bobthecow/psysh/wiki/Magic-variables)
* [⏳ 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)
* [🐛 Troubleshooting](https://github.com/bobthecow/psysh/wiki/Troubleshooting)
### [📢 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)
* [🎨 Themes](https://github.com/bobthecow/psysh/wiki/Themes)
* [📄 Sample config file](https://github.com/bobthecow/psysh/wiki/Sample-config)
### [🔌 Integrations](https://github.com/bobthecow/psysh/wiki/Integrations)

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -e
cd "${BASH_SOURCE%/*}/.."
echo "Building phar"
./bin/build-vendor
php -d 'phar.readonly=0' ./bin/build-phar
echo "Building compat phar"
./bin/build-vendor-compat
php -d 'phar.readonly=0' ./bin/build-phar --compat

View File

@@ -1,313 +0,0 @@
#!/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.
*/
define('WRAP_WIDTH', 100);
$count = 0;
if (count($argv) !== 3 || !is_dir($argv[1])) {
echo "usage: build_manual path/to/manual output_filename.db\n";
exit(1);
}
function htmlwrap($text, $width = null)
{
if ($width === null) {
$width = WRAP_WIDTH;
}
$len = strlen($text);
$return = array();
$lastSpace = null;
$inTag = false;
$i = $tagWidth = 0;
do {
switch (substr($text, $i, 1)) {
case "\n":
$return[] = trim(substr($text, 0, $i));
$text = substr($text, $i);
$len = strlen($text);
$i = $lastSpace = 0;
continue;
case ' ':
if (!$inTag) {
$lastSpace = $i;
}
break;
case '<':
$inTag = true;
break;
case '>':
$inTag = false;
default:
}
if ($inTag) {
$tagWidth++;
}
$i++;
if (!$inTag && ($i - $tagWidth > $width)) {
$lastSpace = $lastSpace ?: $width;
$return[] = trim(substr($text, 0, $lastSpace));
$text = substr($text, $lastSpace);
$len = strlen($text);
$i = $tagWidth = 0;
}
} while ($i < $len);
$return[] = trim($text);
return implode("\n", $return);
}
function extract_paragraphs($element)
{
$paragraphs = array();
foreach ($element->getElementsByTagName('para') as $p) {
$text = '';
foreach ($p->childNodes as $child) {
// @todo figure out if there's something we can do with tables.
if ($child instanceof DOMElement && $child->tagName === 'table') {
continue;
}
// skip references, because ugh.
if (preg_match('{^\s*&[a-z][a-z\.]+;\s*$}', $child->textContent)) {
continue;
}
$text .= $child->ownerDocument->saveXML($child);
}
if ($text = trim(preg_replace('{\n[ \t]+}', ' ', $text))) {
$paragraphs[] = $text;
}
}
return implode("\n\n", $paragraphs);
}
function format_doc($doc)
{
$chunks = array();
if (!empty($doc['description'])) {
$chunks[] = '<comment>Description:</comment>';
$chunks[] = indent_text(htmlwrap(thunk_tags($doc['description']), WRAP_WIDTH - 2));
$chunks[] = '';
}
if (!empty($doc['params'])) {
$chunks[] = '<comment>Param:</comment>';
$typeMax = max(array_map(function ($param) {
return strlen($param['type']);
}, $doc['params']));
$max = max(array_map(function ($param) {
return strlen($param['name']);
}, $doc['params']));
$template = ' <info>%-' . $typeMax . 's</info> <strong>%-' . $max . 's</strong> %s';
$indent = str_repeat(' ', $typeMax + $max + 6);
$wrapWidth = WRAP_WIDTH - strlen($indent);
foreach ($doc['params'] as $param) {
$desc = indent_text(htmlwrap(thunk_tags($param['description']), $wrapWidth), $indent, false);
$chunks[] = sprintf($template, $param['type'], $param['name'], $desc);
}
$chunks[] = '';
}
if (isset($doc['return']) || isset($doc['return_type'])) {
$chunks[] = '<comment>Return:</comment>';
$type = isset($doc['return_type']) ? $doc['return_type'] : 'unknown';
$desc = isset($doc['return']) ? $doc['return'] : '';
$indent = str_repeat(' ', strlen($type) + 4);
$wrapWidth = WRAP_WIDTH - strlen($indent);
if (!empty($desc)) {
$desc = indent_text(htmlwrap(thunk_tags($doc['return']), $wrapWidth), $indent, false);
}
$chunks[] = sprintf(' <info>%s</info> %s', $type, $desc);
$chunks[] = '';
}
array_pop($chunks); // get rid of the trailing newline
return implode("\n", $chunks);
}
function thunk_tags($text)
{
$tagMap = array(
'parameter>' => 'strong>',
'function>' => 'strong>',
'literal>' => 'return>',
'type>' => 'info>',
'constant>' => 'info>',
);
$andBack = array(
'&amp;' => '&',
'&amp;true;' => '<return>true</return>',
'&amp;false;' => '<return>false</return>',
'&amp;null;' => '<return>null</return>',
);
return strtr(strip_tags(strtr($text, $tagMap), '<strong><return><info>'), $andBack);
}
function indent_text($text, $indent = ' ', $leading = true)
{
return ($leading ? $indent : '') . str_replace("\n", "\n" . $indent, $text);
}
function find_type($xml, $paramName)
{
foreach ($xml->getElementsByTagName('methodparam') as $param) {
if ($type = $param->getElementsByTagName('type')->item(0)) {
if ($parameter = $param->getElementsByTagName('parameter')->item(0)) {
if ($paramName === $parameter->textContent) {
return $type->textContent;
}
}
}
}
}
function format_function_doc($xml)
{
$doc = array();
$refsect1s = $xml->getElementsByTagName('refsect1');
foreach ($refsect1s as $refsect1) {
$role = $refsect1->getAttribute('role');
switch ($role) {
case 'description':
$doc['description'] = extract_paragraphs($refsect1);
if ($synopsis = $refsect1->getElementsByTagName('methodsynopsis')->item(0)) {
foreach ($synopsis->childNodes as $node) {
if ($node instanceof DOMElement && $node->tagName === 'type') {
$doc['return_type'] = $node->textContent;
break;
}
}
}
break;
case 'returnvalues':
// do nothing.
$doc['return'] = extract_paragraphs($refsect1);
break;
case 'parameters':
$params = array();
$vars = $refsect1->getElementsByTagName('varlistentry');
foreach ($vars as $var) {
if ($name = $var->getElementsByTagName('parameter')->item(0)) {
$params[] = array(
'name' => '$' . $name->textContent,
'type' => find_type($xml, $name->textContent),
'description' => extract_paragraphs($var),
);
}
}
$doc['params'] = $params;
break;
}
}
// and the purpose
if ($purpose = $xml->getElementsByTagName('refpurpose')->item(0)) {
$desc = htmlwrap($purpose->textContent);
if (isset($doc['description'])) {
$desc .= "\n\n" . $doc['description'];
}
$doc['description'] = trim($desc);
}
$ids = array();
foreach ($xml->getElementsByTagName('refname') as $ref) {
$ids[] = $ref->textContent;
}
return array($ids, format_doc($doc));
}
function format_class_doc($xml)
{
// @todo implement this
return array(array(), null);
}
$dir = new RecursiveDirectoryIterator($argv[1]);
$filter = new RecursiveCallbackFilterIterator($dir, function ($current, $key, $iterator) {
return $current->getFilename()[0] !== '.' &&
($current->isDir() || $current->getExtension() === 'xml') &&
strpos($current->getFilename(), 'entities.') !== 0 &&
$current->getFilename() !== 'pdo_4d'; // Temporarily blacklist this one, the docs are weird.
});
$iterator = new RecursiveIteratorIterator($filter);
$docs = array();
foreach ($iterator as $file) {
$xmlstr = str_replace('&', '&amp;', file_get_contents($file));
$xml = new DOMDocument();
$xml->preserveWhiteSpace = false;
if (!@$xml->loadXml($xmlstr)) {
echo "XML Parse Error: $file\n";
continue;
}
if ($xml->getElementsByTagName('refentry')->length !== 0) {
list($ids, $doc) = format_function_doc($xml);
} elseif ($xml->getElementsByTagName('classref')->length !== 0) {
list($ids, $doc) = format_class_doc($xml);
} else {
$ids = array();
$doc = null;
}
foreach ($ids as $id) {
$docs[$id] = $doc;
}
}
if (is_file($argv[2])) {
unlink($argv[2]);
}
$db = new PDO('sqlite:' . $argv[2]);
$db->query('CREATE TABLE php_manual (id char(256) PRIMARY KEY, doc TEXT)');
$cmd = $db->prepare('INSERT INTO php_manual (id, doc) VALUES (?, ?)');
foreach ($docs as $id => $doc) {
$cmd->execute(array($id, $doc));
}

View File

@@ -1,38 +0,0 @@
#!/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.
*/
if (!is_file(dirname(__DIR__) . '/vendor/autoload.php')) {
throw new RuntimeException('Missing PsySH dev dependencies in ' . dirname(__DIR__) . '/vendor/' . ', install with `composer.phar install --dev`.');
}
require dirname(__DIR__) . '/vendor/autoload.php';
if (!class_exists('Symfony\Component\Finder\Finder')) {
throw new RuntimeException('Missing PsySH dev dependencies, install with `composer.phar install --dev`.');
}
if (!is_file(dirname(__DIR__) . '/build-vendor/autoload.php')) {
throw new RuntimeException('Missing phar vendor dependencies, install with bin/build-vendor');
}
use Psy\Compiler;
error_reporting(-1);
ini_set('display_errors', 1);
$compiler = new Compiler();
if (isset($_SERVER['argv']) && isset($_SERVER['argv'][1]) && $_SERVER['argv'][1] === '--compat') {
$compiler->compile('psysh-compat.phar');
} else {
$compiler->compile();
}

View File

@@ -1,10 +0,0 @@
#!/usr/bin/env bash
set -e
cd "${BASH_SOURCE%/*}/.."
rm -rf build-vendor
COMPOSER_VENDOR_DIR=build-vendor composer update \
--prefer-stable --no-dev --no-progress --classmap-authoritative --no-interaction --verbose

View File

@@ -1,21 +0,0 @@
#!/usr/bin/env bash
set -e
cd "${BASH_SOURCE%/*}/.."
rm -rf build-vendor
rm composer*.lock
cp composer.json composer-compat.json
if [[ $(php --version) = PHP\ 5.3* ]]; then
HOA_VERSION=^1.14
fi
COMPOSER=composer-compat.json COMPOSER_VENDOR_DIR=build-vendor \
composer require symfony/intl hoa/console $HOA_VERSION --no-progress --no-update --no-interaction --verbose
COMPOSER=composer-compat.json COMPOSER_VENDOR_DIR=build-vendor \
composer update --prefer-stable --no-dev --no-progress --classmap-authoritative --no-interaction --verbose

View File

@@ -1,55 +0,0 @@
#!/usr/bin/env bash
set -e
cd "${BASH_SOURCE%/*}/.."
USAGE="usage: bin/package [-v PACKAGE_VERSION]"
while getopts ":v:h" opt; do
case $opt in
v)
PKG_VERSION=$OPTARG
;;
h)
echo $USAGE >&2
exit
;;
\?)
echo "Invalid option: -$OPTARG" >&2
echo $USAGE >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument" >&2
echo $USAGE >&2
exit 1
;;
esac
done
if [ -z "$PKG_VERSION" ]; then
PKG_VERSION=$(git describe --tag --exact-match)
fi
if [[ $(php --version) = PHP\ 5.3* ]]; then
PKG_VERSION=${PKG_VERSION}-php53
fi
echo "Packaging $PKG_VERSION"
mkdir -p dist || exit 1
./bin/build || exit 1
chmod +x *.phar
echo "Creating tarballs"
# Support BSD tar because OS X :(
if [[ $(tar --version) = bsdtar* ]]; then
tar -s "/.*/psysh/" -czf dist/psysh-${PKG_VERSION}.tar.gz psysh.phar
tar -s "/.*/psysh/" -czf dist/psysh-${PKG_VERSION}-compat.tar.gz psysh-compat.phar
else
tar --transform "s/.*/psysh/" -czf dist/psysh-${PKG_VERSION}.tar.gz psysh.phar
tar --transform "s/.*/psysh/" -czf dist/psysh-${PKG_VERSION}-compat.tar.gz psysh-compat.phar
fi

View File

@@ -4,7 +4,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -20,7 +20,7 @@ call_user_func(function () {
foreach ($argv as $i => $arg) {
if ($arg === '--cwd') {
if ($i >= count($argv) - 1) {
echo 'Missing --cwd argument.' . PHP_EOL;
fwrite(STDERR, 'Missing --cwd argument.' . PHP_EOL);
exit(1);
}
$cwd = $argv[$i + 1];
@@ -43,14 +43,21 @@ call_user_func(function () {
$chunks = explode('/', $cwd);
while (!empty($chunks)) {
$path = implode('/', $chunks);
$prettyPath = $path;
if (isset($_SERVER['HOME']) && $_SERVER['HOME']) {
$prettyPath = preg_replace('/^' . preg_quote($_SERVER['HOME'], '/') . '/', '~', $path);
}
// 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.
// We're inside the psysh project. Let's use the local Composer autoload.
if (is_file($path . '/vendor/autoload.php')) {
if (realpath($path) !== realpath(__DIR__ . '/..')) {
fwrite(STDERR, 'Using local PsySH version at ' . $prettyPath . PHP_EOL);
}
require $path . '/vendor/autoload.php';
}
@@ -64,9 +71,12 @@ call_user_func(function () {
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.
// We're inside a project which requires psysh. We'll use the local Composer autoload.
if (is_file($path . '/vendor/autoload.php')) {
if (realpath($path . '/vendor') !== realpath(__DIR__ . '/../../..')) {
fwrite(STDERR, 'Using local PsySH version at ' . $prettyPath . PHP_EOL);
}
require $path . '/vendor/autoload.php';
}
@@ -89,8 +99,8 @@ if (!class_exists('Psy\Shell')) {
} 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;
fwrite(STDERR, 'PsySH dependencies not found, be sure to run `composer install`.' . PHP_EOL);
fwrite(STDERR, 'See https://getcomposer.org to get Composer.' . PHP_EOL);
exit(1);
}
/* >>> */
@@ -98,12 +108,15 @@ if (!class_exists('Psy\Shell')) {
// If the psysh binary was included directly, assume they just wanted an
// autoloader and bail early.
//
// Keep this PHP 5.3 and 5.4 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);
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
}
if (Psy\Shell::isIncluded($trace)) {
@@ -117,17 +130,17 @@ unset($trace);
// If the local version is too old, we can't do this
if (!function_exists('Psy\bin')) {
$argv = $_SERVER['argv'];
$argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : array();
$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;
fwrite(STDERR, 'A local PsySH dependency was found, but it cannot be loaded. Please update to' . PHP_EOL);
fwrite(STDERR, 'the latest version, or run the local copy directly, e.g.:' . PHP_EOL);
fwrite(STDERR, PHP_EOL);
fwrite(STDERR, ' ' . implode(' ', $argv) . PHP_EOL);
exit(1);
}

View File

@@ -13,41 +13,45 @@
}
],
"require": {
"php": ">=5.3.9",
"symfony/console": "~2.3.10|^2.4.2|~3.0",
"symfony/var-dumper": "~2.7|~3.0",
"nikic/php-parser": "~1.3|~2.0|~3.0",
"dnoegel/php-xdg-base-dir": "0.1",
"jakub-onderka/php-console-highlighter": "0.3.*"
"php": "^8.0 || ^7.0.8",
"ext-json": "*",
"ext-tokenizer": "*",
"symfony/console": "^6.0 || ^5.0 || ^4.0 || ^3.4",
"symfony/var-dumper": "^6.0 || ^5.0 || ^4.0 || ^3.4",
"nikic/php-parser": "^4.0 || ^3.1"
},
"require-dev": {
"phpunit/phpunit": "~4.4|~5.0",
"symfony/finder": "~2.1|~3.0",
"friendsofphp/php-cs-fixer": "~1.11",
"hoa/console": "~3.16|~1.14"
"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."
"ext-pdo-sqlite": "The doc command requires SQLite to work."
},
"autoload": {
"files": ["src/Psy/functions.php"],
"files": ["src/functions.php"],
"psr-4": {
"Psy\\": "src/Psy/"
"Psy\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Psy\\Test\\": "test/Psy/Test/"
"Psy\\Test\\": "test/"
}
},
"bin": ["bin/psysh"],
"config": {
"allow-plugins": {
"bamarni/composer-bin-plugin": true
}
},
"extra": {
"branch-alias": {
"dev-develop": "0.8.x-dev"
"dev-main": "0.11.x-dev"
}
},
"conflict": {
"symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4"
}
}

View File

@@ -1,12 +0,0 @@
<?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/Psy</directory>
</whitelist>
</filter>
</phpunit>

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

@@ -0,0 +1,399 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy;
use PhpParser\NodeTraverser;
use PhpParser\Parser;
use PhpParser\PrettyPrinter\Standard as Printer;
use Psy\CodeCleaner\AbstractClassPass;
use Psy\CodeCleaner\AssignThisVariablePass;
use Psy\CodeCleaner\CalledClassPass;
use Psy\CodeCleaner\CallTimePassByReferencePass;
use Psy\CodeCleaner\EmptyArrayDimFetchPass;
use Psy\CodeCleaner\ExitPass;
use Psy\CodeCleaner\FinalClassPass;
use Psy\CodeCleaner\FunctionContextPass;
use Psy\CodeCleaner\FunctionReturnInWriteContextPass;
use Psy\CodeCleaner\ImplicitReturnPass;
use Psy\CodeCleaner\InstanceOfPass;
use Psy\CodeCleaner\IssetPass;
use Psy\CodeCleaner\LabelContextPass;
use Psy\CodeCleaner\LeavePsyshAlonePass;
use Psy\CodeCleaner\ListPass;
use Psy\CodeCleaner\LoopContextPass;
use Psy\CodeCleaner\MagicConstantsPass;
use Psy\CodeCleaner\NamespacePass;
use Psy\CodeCleaner\PassableByReferencePass;
use Psy\CodeCleaner\RequirePass;
use Psy\CodeCleaner\ReturnTypePass;
use Psy\CodeCleaner\StrictTypesPass;
use Psy\CodeCleaner\UseStatementPass;
use Psy\CodeCleaner\ValidClassNamePass;
use Psy\CodeCleaner\ValidConstructorPass;
use Psy\CodeCleaner\ValidFunctionNamePass;
use Psy\Exception\ParseErrorException;
/**
* A service to clean up user input, detect parse errors before they happen,
* and generally work around issues with the PHP code evaluation experience.
*/
class CodeCleaner
{
private $yolo = false;
private $parser;
private $printer;
private $traverser;
private $namespace;
/**
* CodeCleaner constructor.
*
* @param Parser|null $parser A PhpParser Parser instance. One will be created if not explicitly supplied
* @param Printer|null $printer A PhpParser Printer instance. One will be created if not explicitly supplied
* @param NodeTraverser|null $traverser A PhpParser NodeTraverser instance. One will be created if not explicitly supplied
* @param bool $yolo run without input validation
*/
public function __construct(Parser $parser = null, Printer $printer = null, NodeTraverser $traverser = null, bool $yolo = false)
{
$this->yolo = $yolo;
if ($parser === null) {
$parserFactory = new ParserFactory();
$parser = $parserFactory->createParser();
}
$this->parser = $parser;
$this->printer = $printer ?: new Printer();
$this->traverser = $traverser ?: new NodeTraverser();
foreach ($this->getDefaultPasses() as $pass) {
$this->traverser->addVisitor($pass);
}
}
/**
* Check whether this CodeCleaner is in YOLO mode.
*
* @return bool
*/
public function yolo(): bool
{
return $this->yolo;
}
/**
* Get default CodeCleaner passes.
*
* @return array
*/
private function getDefaultPasses(): array
{
if ($this->yolo) {
return $this->getYoloPasses();
}
$useStatementPass = new UseStatementPass();
$namespacePass = new NamespacePass($this);
// Try to add implicit `use` statements and an implicit namespace,
// based on the file in which the `debug` call was made.
$this->addImplicitDebugContext([$useStatementPass, $namespacePass]);
return [
// Validation passes
new AbstractClassPass(),
new AssignThisVariablePass(),
new CalledClassPass(),
new CallTimePassByReferencePass(),
new FinalClassPass(),
new FunctionContextPass(),
new FunctionReturnInWriteContextPass(),
new InstanceOfPass(),
new IssetPass(),
new LabelContextPass(),
new LeavePsyshAlonePass(),
new ListPass(),
new LoopContextPass(),
new PassableByReferencePass(),
new ReturnTypePass(),
new EmptyArrayDimFetchPass(),
new ValidConstructorPass(),
// Rewriting shenanigans
$useStatementPass, // must run before the namespace pass
new ExitPass(),
new ImplicitReturnPass(),
new MagicConstantsPass(),
$namespacePass, // must run after the implicit return pass
new RequirePass(),
new StrictTypesPass(),
// Namespace-aware validation (which depends on aforementioned shenanigans)
new ValidClassNamePass(),
new ValidFunctionNamePass(),
];
}
/**
* A set of code cleaner passes that don't try to do any validation, and
* only do minimal rewriting to make things work inside the REPL.
*
* This list should stay in sync with the "rewriting shenanigans" in
* getDefaultPasses above.
*
* @return array
*/
private function getYoloPasses(): array
{
$useStatementPass = new UseStatementPass();
$namespacePass = new NamespacePass($this);
// Try to add implicit `use` statements and an implicit namespace,
// based on the file in which the `debug` call was made.
$this->addImplicitDebugContext([$useStatementPass, $namespacePass]);
return [
new LeavePsyshAlonePass(),
$useStatementPass, // must run before the namespace pass
new ExitPass(),
new ImplicitReturnPass(),
new MagicConstantsPass(),
$namespacePass, // must run after the implicit return pass
new RequirePass(),
new StrictTypesPass(),
];
}
/**
* "Warm up" code cleaner passes when we're coming from a debug call.
*
* This is useful, for example, for `UseStatementPass` and `NamespacePass`
* which keep track of state between calls, to maintain the current
* namespace and a map of use statements.
*
* @param array $passes
*/
private function addImplicitDebugContext(array $passes)
{
$file = $this->getDebugFile();
if ($file === null) {
return;
}
try {
$code = @\file_get_contents($file);
if (!$code) {
return;
}
$stmts = $this->parse($code, true);
if ($stmts === false) {
return;
}
// Set up a clean traverser for just these code cleaner passes
$traverser = new NodeTraverser();
foreach ($passes as $pass) {
$traverser->addVisitor($pass);
}
$traverser->traverse($stmts);
} catch (\Throwable $e) {
// Don't care.
}
}
/**
* 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): bool
{
$class = isset($stackFrame['class']) ? $stackFrame['class'] : null;
$function = isset($stackFrame['function']) ? $stackFrame['function'] : null;
return ($class === null && $function === 'Psy\\debug') ||
($class === Shell::class && $function === 'debug');
}
/**
* Clean the given array of code.
*
* @throws ParseErrorException if the code is invalid PHP, and cannot be coerced into valid PHP
*
* @param array $codeLines
* @param bool $requireSemicolons
*
* @return string|false Cleaned PHP code, False if the input is incomplete
*/
public function clean(array $codeLines, bool $requireSemicolons = false)
{
$stmts = $this->parse('<?php '.\implode(\PHP_EOL, $codeLines).\PHP_EOL, $requireSemicolons);
if ($stmts === false) {
return false;
}
// Catch fatal errors before they happen
$stmts = $this->traverser->traverse($stmts);
// Work around https://github.com/nikic/PHP-Parser/issues/399
$oldLocale = \setlocale(\LC_NUMERIC, 0);
\setlocale(\LC_NUMERIC, 'C');
$code = $this->printer->prettyPrint($stmts);
// Now put the locale back
\setlocale(\LC_NUMERIC, $oldLocale);
return $code;
}
/**
* Set the current local namespace.
*
* @param array|null $namespace (default: null)
*
* @return array|null
*/
public function setNamespace(array $namespace = null)
{
$this->namespace = $namespace;
}
/**
* Get the current local namespace.
*
* @return array|null
*/
public function getNamespace()
{
return $this->namespace;
}
/**
* Lex and parse a block of code.
*
* @see Parser::parse
*
* @throws ParseErrorException for parse errors that can't be resolved by
* waiting a line to see what comes next
*
* @param string $code
* @param bool $requireSemicolons
*
* @return array|false A set of statements, or false if incomplete
*/
protected function parse(string $code, bool $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): bool
{
$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, string $code): bool
{
if ($e->getRawMessage() !== 'Syntax error, unexpected T_ENCAPSED_AND_WHITESPACE') {
return false;
}
try {
$this->parser->parse($code."';");
} catch (\Throwable $e) {
return false;
}
return true;
}
private function parseErrorIsUnterminatedComment(\PhpParser\Error $e, $code): bool
{
return $e->getRawMessage() === 'Unterminated comment';
}
private function parseErrorIsTrailingComma(\PhpParser\Error $e, $code): bool
{
return ($e->getRawMessage() === 'A trailing comma is not allowed here') && (\substr(\rtrim($code), -1) === ',');
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -25,46 +25,50 @@ class AbstractClassPass extends CodeCleanerPass
private $abstractMethods;
/**
* @throws RuntimeException if the node is an abstract function with a body
* @throws FatalErrorException if the node is an abstract function with a body
*
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
if ($node instanceof Class_) {
$this->class = $node;
$this->abstractMethods = array();
$this->abstractMethods = [];
} elseif ($node instanceof ClassMethod) {
if ($node->isAbstract()) {
$name = sprintf('%s::%s', $this->class->name, $node->name);
$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());
$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
* @throws FatalErrorException if the node is a non-abstract class with abstract methods
*
* @param Node $node
*
* @return int|Node|Node[]|null Replacement node (or special return value)
*/
public function leaveNode(Node $node)
{
if ($node instanceof Class_) {
$count = count($this->abstractMethods);
$count = \count($this->abstractMethods);
if ($count > 0 && !$node->isAbstract()) {
$msg = sprintf(
$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)
\implode(', ', $this->abstractMethods)
);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine());
}
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -26,14 +26,16 @@ class AssignThisVariablePass extends CodeCleanerPass
/**
* Validate that the user input does not assign the `$this` variable.
*
* @throws RuntimeException if the user assign the `$this` variable
* @throws FatalErrorException if the user assign the `$this` variable
*
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
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());
throw new FatalErrorException('Cannot re-assign $this', 0, \E_ERROR, null, $node->getLine());
}
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -15,6 +15,7 @@ use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\VariadicPlaceholder;
use Psy\Exception\FatalErrorException;
/**
@@ -31,23 +32,25 @@ class CallTimePassByReferencePass extends CodeCleanerPass
/**
* Validate of use call-time pass-by-reference.
*
* @throws RuntimeException if the user used call-time pass-by-reference in PHP >= 5.4.0
* @throws FatalErrorException if the user used call-time pass-by-reference
*
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
if (version_compare(PHP_VERSION, '5.4', '<')) {
return;
}
if (!$node instanceof FuncCall && !$node instanceof MethodCall && !$node instanceof StaticCall) {
return;
}
foreach ($node->args as $arg) {
if ($arg instanceof VariadicPlaceholder) {
continue;
}
if ($arg->byRef) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getLine());
}
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -17,6 +17,7 @@ use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\Node\VariadicPlaceholder;
use Psy\Exception\ErrorException;
/**
@@ -29,6 +30,8 @@ class CalledClassPass extends CodeCleanerPass
/**
* @param array $nodes
*
* @return Node[]|null Array of nodes
*/
public function beforeTraverse(array $nodes)
{
@@ -39,6 +42,8 @@ class CalledClassPass extends CodeCleanerPass
* @throws ErrorException if get_class or get_called_class is called without an object from outside a class
*
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
@@ -58,16 +63,18 @@ class CalledClassPass extends CodeCleanerPass
return;
}
$name = strtolower($node->name);
if (in_array($name, array('get_class', 'get_called_class'))) {
$msg = sprintf('%s() called without object from outside a class', $name);
throw new ErrorException($msg, 0, E_USER_WARNING, null, $node->getLine());
$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
*
* @return int|Node|Node[]|null Replacement node (or special return value)
*/
public function leaveNode(Node $node)
{
@@ -76,8 +83,12 @@ class CalledClassPass extends CodeCleanerPass
}
}
private function isNull(Node $node)
private function isNull(Node $node): bool
{
return $node->value instanceof ConstFetch && strtolower($node->value->name) === 'null';
if ($node instanceof VariadicPlaceholder) {
return false;
}
return $node->value instanceof ConstFetch && \strtolower($node->value->name) === 'null';
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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\ArrayDimFetch;
use PhpParser\Node\Expr\Assign;
use Psy\Exception\FatalErrorException;
/**
* Validate empty brackets are only used for assignment.
*/
class EmptyArrayDimFetchPass extends CodeCleanerPass
{
const EXCEPTION_MESSAGE = 'Cannot use [] for reading';
private $theseOnesAreFine = [];
/**
* @return Node[]|null Array of nodes
*/
public function beforeTraverse(array $nodes)
{
$this->theseOnesAreFine = [];
}
/**
* @throws FatalErrorException if the user used empty empty array dim fetch outside of assignment
*
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
if ($node instanceof Assign && $node->var instanceof ArrayDimFetch) {
$this->theseOnesAreFine[] = $node->var;
}
if ($node instanceof ArrayDimFetch && $node->dim === null) {
if (!\in_array($node, $this->theseOnesAreFine)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, $node->getLine());
}
}
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -15,6 +15,7 @@ use PhpParser\Node;
use PhpParser\Node\Expr\Exit_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
use Psy\Exception\BreakException;
class ExitPass extends CodeCleanerPass
{
@@ -22,11 +23,13 @@ class ExitPass extends CodeCleanerPass
* Converts exit calls to BreakExceptions.
*
* @param \PhpParser\Node $node
*
* @return int|Node|Node[]|null Replacement node (or special return value)
*/
public function leaveNode(Node $node)
{
if ($node instanceof Exit_) {
return new StaticCall(new FullyQualifiedName('Psy\Exception\BreakException'), 'exitShell');
return new StaticCall(new FullyQualifiedName(BreakException::class), 'exitShell');
}
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -24,16 +24,20 @@ class FinalClassPass extends CodeCleanerPass
/**
* @param array $nodes
*
* @return Node[]|null Array of nodes
*/
public function beforeTraverse(array $nodes)
{
$this->finalClasses = array();
$this->finalClasses = [];
}
/**
* @throws RuntimeException if the node is a class that extends a final class
* @throws FatalErrorException if the node is a class that extends a final class
*
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
@@ -41,13 +45,13 @@ class FinalClassPass extends CodeCleanerPass
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());
$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;
$this->finalClasses[\strtolower($node->name)] = true;
}
}
}
@@ -57,10 +61,10 @@ class FinalClassPass extends CodeCleanerPass
*
* @return bool
*/
private function isFinalClass($name)
private function isFinalClass(string $name): bool
{
if (!class_exists($name)) {
return isset($this->finalClasses[strtolower($name)]);
if (!\class_exists($name)) {
return isset($this->finalClasses[\strtolower($name)]);
}
$refl = new \ReflectionClass($name);

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -23,12 +23,17 @@ class FunctionContextPass extends CodeCleanerPass
/**
* @param array $nodes
*
* @return Node[]|null Array of nodes
*/
public function beforeTraverse(array $nodes)
{
$this->functionDepth = 0;
}
/**
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
if ($node instanceof FunctionLike) {
@@ -45,12 +50,14 @@ class FunctionContextPass extends CodeCleanerPass
// 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());
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine());
}
}
/**
* @param \PhpParser\Node $node
*
* @return int|Node|Node[]|null Replacement node (or special return value)
*/
public function leaveNode(Node $node)
{

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -14,11 +14,12 @@ 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 PhpParser\Node\VariadicPlaceholder;
use Psy\Exception\FatalErrorException;
/**
@@ -28,52 +29,48 @@ use Psy\Exception\FatalErrorException;
*/
class FunctionReturnInWriteContextPass extends CodeCleanerPass
{
const PHP55_MESSAGE = 'Cannot use isset() on the result of a function call (you can use "null !== func()" instead)';
const ISSET_MESSAGE = 'Cannot use isset() on the result of an expression (you can use "null !== expression" instead)';
const EXCEPTION_MESSAGE = "Can't use function return value in write context";
private $isPhp55;
public function __construct()
{
$this->isPhp55 = version_compare(PHP_VERSION, '5.5', '>=');
}
/**
* Validate that the functions are used correctly.
*
* @throws FatalErrorException if a function is passed as an argument reference
* @throws FatalErrorException if a function is used as an argument in the isset
* @throws FatalErrorException if a function is used as an argument in the empty, only for PHP < 5.5
* @throws FatalErrorException if a value is assigned to a function
*
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
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->byRef && $this->isCallNode($item->value)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
if ($item instanceof VariadicPlaceholder) {
continue;
}
if ($item && $item->byRef && $this->isCallNode($item->value)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getLine());
}
}
} elseif ($node instanceof Isset_) {
} elseif ($node instanceof Isset_ || $node instanceof Unset_) {
foreach ($node->vars as $var) {
if (!$this->isCallNode($var)) {
continue;
}
$msg = $this->isPhp55 ? self::PHP55_MESSAGE : self::EXCEPTION_MESSAGE;
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
$msg = $node instanceof Isset_ ? self::ISSET_MESSAGE : self::EXCEPTION_MESSAGE;
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine());
}
} elseif ($node instanceof Empty_ && !$this->isPhp55 && $this->isCallNode($node->expr)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
} elseif ($node instanceof Assign && $this->isCallNode($node->var)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getLine());
}
}
private function isCallNode(Node $node)
private function isCallNode(Node $node): bool
{
return $node instanceof FuncCall || $node instanceof MethodCall || $node instanceof StaticCall;
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,12 +11,12 @@
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Exit_;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Return_;
@@ -32,7 +32,7 @@ class ImplicitReturnPass extends CodeCleanerPass
*
* @return array
*/
public function beforeTraverse(array $nodes)
public function beforeTraverse(array $nodes): array
{
return $this->addImplicitReturn($nodes);
}
@@ -42,14 +42,14 @@ class ImplicitReturnPass extends CodeCleanerPass
*
* @return array
*/
private function addImplicitReturn(array $nodes)
private function addImplicitReturn(array $nodes): array
{
// If nodes is empty, it can't have a return value.
if (empty($nodes)) {
return array(new Return_(new New_(new FullyQualifiedName('Psy\CodeCleaner\NoReturnValue'))));
return [new Return_(NoReturnValue::create())];
}
$last = end($nodes);
$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)
@@ -68,17 +68,25 @@ class ImplicitReturnPass extends CodeCleanerPass
} elseif ($last instanceof Switch_) {
foreach ($last->cases as $case) {
// only add an implicit return to cases which end in break
$caseLast = end($case->stmts);
$caseLast = \end($case->stmts);
if ($caseLast instanceof Break_) {
$case->stmts = $this->addImplicitReturn(array_slice($case->stmts, 0, -1));
$case->stmts = $this->addImplicitReturn(\array_slice($case->stmts, 0, -1));
$case->stmts[] = $caseLast;
}
}
} elseif ($last instanceof Expr && !($last instanceof Exit_)) {
$nodes[count($nodes) - 1] = new Return_($last, array(
// @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);
}
@@ -93,10 +101,28 @@ class ImplicitReturnPass extends CodeCleanerPass
// We're not adding a fallback return after namespace statements,
// because code outside namespace statements doesn't really work, and
// there's already an implicit return in the namespace statement anyway.
if ($last instanceof Stmt && !$last instanceof Return_ && !$last instanceof Namespace_) {
$nodes[] = new Return_(new New_(new FullyQualifiedName('Psy\CodeCleaner\NoReturnValue')));
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): bool
{
return $node instanceof Stmt &&
!$node instanceof Expression &&
!$node instanceof Return_ &&
!$node instanceof Namespace_;
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -12,6 +12,9 @@
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\Instanceof_;
use PhpParser\Node\Scalar;
@@ -27,21 +30,40 @@ class InstanceOfPass extends CodeCleanerPass
{
const EXCEPTION_MSG = 'instanceof expects an object instance, constant given';
private $atLeastPhp73;
public function __construct()
{
$this->atLeastPhp73 = \version_compare(\PHP_VERSION, '7.3', '>=');
}
/**
* 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
*
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
// Basically everything is allowed in PHP 7.3 :)
if ($this->atLeastPhp73) {
return;
}
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());
if (($node->expr instanceof Scalar && !$node->expr instanceof Encapsed) ||
$node->expr instanceof BinaryOp ||
$node->expr instanceof Array_ ||
$node->expr instanceof ConstFetch ||
$node->expr instanceof ClassConstFetch
) {
throw new FatalErrorException(self::EXCEPTION_MSG, 0, \E_ERROR, null, $node->getLine());
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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\ArrayDimFetch;
use PhpParser\Node\Expr\Isset_;
use PhpParser\Node\Expr\NullsafePropertyFetch;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use Psy\Exception\FatalErrorException;
/**
* Code cleaner pass to ensure we only allow variables, array fetch and property
* fetch expressions in isset() calls.
*/
class IssetPass extends CodeCleanerPass
{
const EXCEPTION_MSG = 'Cannot use isset() on the result of an expression (you can use "null !== expression" instead)';
/**
* @throws FatalErrorException
*
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
if (!$node instanceof Isset_) {
return;
}
foreach ($node->vars as $var) {
if (!$var instanceof Variable && !$var instanceof ArrayDimFetch && !$var instanceof PropertyFetch && !$var instanceof NullsafePropertyFetch) {
throw new FatalErrorException(self::EXCEPTION_MSG, 0, \E_ERROR, null, $node->getLine());
}
}
}
}

View File

@@ -0,0 +1,101 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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\FunctionLike;
use PhpParser\Node\Stmt\Goto_;
use PhpParser\Node\Stmt\Label;
use Psy\Exception\FatalErrorException;
/**
* CodeCleanerPass for label context.
*
* This class partially emulates the PHP label specification.
* PsySH can not declare labels by sequentially executing lines with eval,
* but since it is not a syntax error, no error is raised.
* This class warns before invalid goto causes a fatal error.
* Since this is a simple checker, it does not block real fatal error
* with complex syntax. (ex. it does not parse inside function.)
*
* @see http://php.net/goto
*/
class LabelContextPass extends CodeCleanerPass
{
/** @var int */
private $functionDepth;
/** @var array */
private $labelDeclarations;
/** @var array */
private $labelGotos;
/**
* @param array $nodes
*
* @return Node[]|null Array of nodes
*/
public function beforeTraverse(array $nodes)
{
$this->functionDepth = 0;
$this->labelDeclarations = [];
$this->labelGotos = [];
}
/**
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
if ($node instanceof FunctionLike) {
$this->functionDepth++;
return;
}
// node is inside function context
if ($this->functionDepth !== 0) {
return;
}
if ($node instanceof Goto_) {
$this->labelGotos[\strtolower($node->name)] = $node->getLine();
} elseif ($node instanceof Label) {
$this->labelDeclarations[\strtolower($node->name)] = $node->getLine();
}
}
/**
* @param \PhpParser\Node $node
*
* @return int|Node|Node[]|null Replacement node (or special return value)
*/
public function leaveNode(Node $node)
{
if ($node instanceof FunctionLike) {
$this->functionDepth--;
}
}
/**
* @return Node[]|null Array of nodes
*/
public function afterTraverse(array $nodes)
{
foreach ($this->labelGotos as $name => $line) {
if (!isset($this->labelDeclarations[$name])) {
$msg = "'goto' to undefined label '{$name}'";
throw new FatalErrorException($msg, 0, \E_ERROR, null, $line);
}
}
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -26,11 +26,13 @@ class LeavePsyshAlonePass extends CodeCleanerPass
* @throws RuntimeException if the user is messing with $__psysh__
*
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
if ($node instanceof Variable && $node->name === '__psysh__') {
throw new RuntimeException('Don\'t mess with $__psysh__. Bad things will happen.');
throw new RuntimeException('Don\'t mess with $__psysh__; bad things will happen');
}
}
}

View File

@@ -0,0 +1,114 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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
*
* @return int|Node|null Replacement node (or special return value)
*/
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): bool
{
$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

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -28,16 +28,12 @@ use Psy\Exception\FatalErrorException;
*/
class LoopContextPass extends CodeCleanerPass
{
private $isPHP54;
private $loopDepth;
public function __construct()
{
$this->isPHP54 = version_compare(PHP_VERSION, '5.4.0', '>=');
}
/**
* {@inheritdoc}
*
* @return Node[]|null Array of nodes
*/
public function beforeTraverse(array $nodes)
{
@@ -51,6 +47,8 @@ class LoopContextPass extends CodeCleanerPass
* @throws FatalErrorException if the node is a break or continue and has an argument less than 1
*
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
@@ -68,24 +66,24 @@ class LoopContextPass extends CodeCleanerPass
$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());
$msg = \sprintf("'%s' not in the 'loop' or 'switch' context", $operator);
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine());
}
if ($node->num instanceof LNumber || $node->num instanceof DNumber) {
$num = $node->num->value;
if ($this->isPHP54 && ($node->num instanceof DNumber || $num < 1)) {
$msg = sprintf("'%s' operator accepts only positive numbers", $operator);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
if ($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());
$msg = \sprintf("Cannot '%s' %d levels", $operator, $num);
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine());
}
} elseif ($node->num && $this->isPHP54) {
$msg = sprintf("'%s' operator with non-constant operand is no longer supported", $operator);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
} 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;
}
@@ -93,6 +91,8 @@ class LoopContextPass extends CodeCleanerPass
/**
* @param Node $node
*
* @return int|Node|Node[]|null Replacement node (or special return value)
*/
public function leaveNode(Node $node)
{

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -29,12 +29,12 @@ class MagicConstantsPass extends CodeCleanerPass
*
* @param Node $node
*
* @return null|FuncCall|String_
* @return FuncCall|String_|null
*/
public function enterNode(Node $node)
{
if ($node instanceof Dir) {
return new FuncCall(new Name('getcwd'), array(), $node->getAttributes());
return new FuncCall(new Name('getcwd'), [], $node->getAttributes());
} elseif ($node instanceof File) {
return new String_('', $node->getAttributes());
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -29,11 +29,13 @@ abstract class NamespaceAwarePass extends CodeCleanerPass
* use afterTraverse or call parent::beforeTraverse() when overloading.
*
* Reset the namespace and the current scope before beginning analysis
*
* @return Node[]|null Array of nodes
*/
public function beforeTraverse(array $nodes)
{
$this->namespace = array();
$this->currentScope = array();
$this->namespace = [];
$this->currentScope = [];
}
/**
@@ -41,11 +43,13 @@ abstract class NamespaceAwarePass extends CodeCleanerPass
* leaveNode or call parent::enterNode() when overloading
*
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
if ($node instanceof Namespace_) {
$this->namespace = isset($node->name) ? $node->name->parts : array();
$this->namespace = isset($node->name) ? $node->name->parts : [];
}
}
@@ -56,16 +60,16 @@ abstract class NamespaceAwarePass extends CodeCleanerPass
*
* @return string
*/
protected function getFullyQualifiedName($name)
protected function getFullyQualifiedName($name): string
{
if ($name instanceof FullyQualifiedName) {
return implode('\\', $name->parts);
return \implode('\\', $name->parts);
} elseif ($name instanceof Name) {
$name = $name->parts;
} elseif (!is_array($name)) {
$name = array($name);
} elseif (!\is_array($name)) {
$name = [$name];
}
return implode('\\', array_merge($this->namespace, $name));
return \implode('\\', \array_merge($this->namespace, $name));
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,6 +11,7 @@
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Namespace_;
use Psy\CodeCleaner;
@@ -46,6 +47,8 @@ class NamespacePass extends CodeCleanerPass
* is encountered.
*
* @param array $nodes
*
* @return Node[]|null Array of nodes
*/
public function beforeTraverse(array $nodes)
{
@@ -53,21 +56,32 @@ class NamespacePass extends CodeCleanerPass
return $nodes;
}
$last = end($nodes);
if (!$last instanceof Namespace_) {
return $this->namespace ? array(new Namespace_($this->namespace, $nodes)) : $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;
}
$this->setNamespace($last->name);
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
* @param Name|null $namespace
*/
private function setNamespace($namespace)
{

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,6 +11,9 @@
namespace Psy\CodeCleaner;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
/**
* A class used internally by CodeCleaner to represent input, such as
* non-expression statements, with no return value.
@@ -20,5 +23,13 @@ namespace Psy\CodeCleaner;
*/
class NoReturnValue
{
// this space intentionally left blank
/**
* Get PhpParser AST expression for creating a new NoReturnValue.
*
* @return New_
*/
public static function create(): New_
{
return new New_(new FullyQualifiedName(self::class));
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -13,6 +13,8 @@ namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
@@ -32,6 +34,8 @@ class PassableByReferencePass extends CodeCleanerPass
* @throws FatalErrorException if non-variables are passed by reference
*
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
@@ -56,18 +60,23 @@ class PassableByReferencePass extends CodeCleanerPass
}
foreach ($refl->getParameters() as $key => $param) {
if (array_key_exists($key, $node->args)) {
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());
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getLine());
}
}
}
}
}
private function isPassableByReference(Node $arg)
private function isPassableByReference(Node $arg): bool
{
// Unpacked arrays can be passed by reference
if ($arg->value instanceof Array_) {
return $arg->unpack;
}
// FuncCall, MethodCall and StaticCall are all PHP _warnings_ not fatal errors, so we'll let
// PHP handle those ones :)
return $arg->value instanceof ClassConstFetch ||
@@ -75,7 +84,8 @@ class PassableByReferencePass extends CodeCleanerPass
$arg->value instanceof Variable ||
$arg->value instanceof FuncCall ||
$arg->value instanceof MethodCall ||
$arg->value instanceof StaticCall;
$arg->value instanceof StaticCall ||
$arg->value instanceof ArrayDimFetch;
}
/**
@@ -102,7 +112,7 @@ class PassableByReferencePass extends CodeCleanerPass
} 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());
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getLine());
}
}
}

View File

@@ -0,0 +1,133 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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;
/**
* Add runtime validation for `require` and `require_once` calls.
*/
class RequirePass extends CodeCleanerPass
{
private static $requireTypes = [Include_::TYPE_REQUIRE, Include_::TYPE_REQUIRE_ONCE];
/**
* {@inheritdoc}
*
* @return int|Node|null Replacement node (or special return value)
*/
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(self::class),
'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.
*
* If $file collides with a path in the currently running PsySH phar, it will be resolved
* relative to the include path, to prevent PHP from grabbing the phar version of the file.
*
* @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, unless $file collides with a path in the currently running phar
*/
public static function resolve($file, $lineNumber = null): string
{
$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);
}
$resolvedPath = \stream_resolve_include_path($file);
if ($file === '' || !$resolvedPath) {
$msg = \sprintf("Failed opening required '%s'", $file);
throw new FatalErrorException($msg, 0, \E_ERROR, null, $lineNumber);
}
// Special case: if the path is not already relative or absolute, and it would resolve to
// something inside the currently running phar (e.g. `vendor/autoload.php`), we'll resolve
// it relative to the include path so PHP won't grab the phar version.
//
// Note that this only works if the phar has `psysh` in the path. We might want to lift this
// restriction and special case paths that would collide with any running phar?
if ($resolvedPath !== $file && $file[0] !== '.') {
$runningPhar = \Phar::running();
if (\strpos($runningPhar, 'psysh') !== false && \is_file($runningPhar.\DIRECTORY_SEPARATOR.$file)) {
foreach (self::getIncludePath() as $prefix) {
$resolvedPath = $prefix.\DIRECTORY_SEPARATOR.$file;
if (\is_file($resolvedPath)) {
return $resolvedPath;
}
}
}
}
return $file;
}
private function isRequireNode(Node $node): bool
{
return $node instanceof Include_ && \in_array($node->type, self::$requireTypes);
}
private static function getIncludePath(): array
{
if (\PATH_SEPARATOR === ':') {
return \preg_split('#:(?!//)#', \get_include_path());
}
return \explode(\PATH_SEPARATOR, \get_include_path());
}
}

View File

@@ -0,0 +1,127 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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\Closure;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Identifier;
use PhpParser\Node\NullableType;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\UnionType;
use Psy\Exception\FatalErrorException;
/**
* Add runtime validation for return types.
*/
class ReturnTypePass extends CodeCleanerPass
{
const MESSAGE = 'A function with return type must return a value';
const NULLABLE_MESSAGE = 'A function with return type must return a value (did you mean "return null;" instead of "return;"?)';
const VOID_MESSAGE = 'A void function must not return a value';
const VOID_NULL_MESSAGE = 'A void function must not return a value (did you mean "return;" instead of "return null;"?)';
const NULLABLE_VOID_MESSAGE = 'Void type cannot be nullable';
private $atLeastPhp71;
private $returnTypeStack = [];
public function __construct()
{
$this->atLeastPhp71 = \version_compare(\PHP_VERSION, '7.1', '>=');
}
/**
* {@inheritdoc}
*
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
if (!$this->atLeastPhp71) {
return; // @codeCoverageIgnore
}
if ($this->isFunctionNode($node)) {
$this->returnTypeStack[] = $node->returnType;
return;
}
if (!empty($this->returnTypeStack) && $node instanceof Return_) {
$expectedType = \end($this->returnTypeStack);
if ($expectedType === null) {
return;
}
$msg = null;
if ($this->typeName($expectedType) === 'void') {
// Void functions
if ($expectedType instanceof NullableType) {
$msg = self::NULLABLE_VOID_MESSAGE;
} elseif ($node->expr instanceof ConstFetch && \strtolower($node->expr->name) === 'null') {
$msg = self::VOID_NULL_MESSAGE;
} elseif ($node->expr !== null) {
$msg = self::VOID_MESSAGE;
}
} else {
// Everything else
if ($node->expr === null) {
$msg = $expectedType instanceof NullableType ? self::NULLABLE_MESSAGE : self::MESSAGE;
}
}
if ($msg !== null) {
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine());
}
}
}
/**
* {@inheritdoc}
*
* @return int|Node|Node[]|null Replacement node (or special return value)
*/
public function leaveNode(Node $node)
{
if (!$this->atLeastPhp71) {
return; // @codeCoverageIgnore
}
if (!empty($this->returnTypeStack) && $this->isFunctionNode($node)) {
\array_pop($this->returnTypeStack);
}
}
private function isFunctionNode(Node $node): bool
{
return $node instanceof Function_ || $node instanceof Closure;
}
private function typeName(Node $node): string
{
if ($node instanceof UnionType) {
return \implode('|', \array_map([$this, 'typeName'], $node->types));
}
if ($node instanceof NullableType) {
return \strtolower($node->type->name);
}
if ($node instanceof Identifier) {
return \strtolower($node->name);
}
throw new \InvalidArgumentException('Unable to find type name');
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,6 +11,8 @@
namespace Psy\CodeCleaner;
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt\Declare_;
use PhpParser\Node\Stmt\DeclareDeclare;
@@ -41,22 +43,22 @@ class StrictTypesPass extends CodeCleanerPass
* @throws FatalErrorException if an invalid `strict_types` declaration is found
*
* @param array $nodes
*
* @return Node[]|null Array of nodes
*/
public function beforeTraverse(array $nodes)
{
if (version_compare(PHP_VERSION, '7.0', '<')) {
return;
}
$prependStrictTypes = $this->strictTypes;
foreach ($nodes as $key => $node) {
foreach ($nodes as $node) {
if ($node instanceof Declare_) {
foreach ($node->declares as $declare) {
if ($declare->key === 'strict_types') {
// 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());
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getLine());
}
$this->strictTypes = $value->value === 1;
@@ -66,10 +68,10 @@ class StrictTypesPass extends CodeCleanerPass
}
if ($prependStrictTypes) {
$first = reset($nodes);
$first = \reset($nodes);
if (!$first instanceof Declare_) {
$declare = new Declare_(array(new DeclareDeclare('strict_types', new LNumber(1))));
array_unshift($nodes, $declare);
$declare = new Declare_([new DeclareDeclare('strict_types', new LNumber(1))]);
\array_unshift($nodes, $declare);
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -17,6 +17,8 @@ use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
use PhpParser\Node\Stmt\GroupUse;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Use_;
use PhpParser\Node\Stmt\UseUse;
use PhpParser\NodeTraverser;
/**
* Provide implicit use statements for subsequent execution.
@@ -30,8 +32,8 @@ use PhpParser\Node\Stmt\Use_;
*/
class UseStatementPass extends CodeCleanerPass
{
private $aliases = array();
private $lastAliases = array();
private $aliases = [];
private $lastAliases = [];
private $lastNamespace = null;
/**
@@ -42,13 +44,15 @@ class UseStatementPass extends CodeCleanerPass
* work like you'd expect.
*
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
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)) {
if (\strtolower($node->name ?: '') === \strtolower($this->lastNamespace ?: '')) {
$this->aliases = $this->lastAliases;
}
}
@@ -61,45 +65,58 @@ class UseStatementPass extends CodeCleanerPass
* remembered aliases to the code.
*
* @param Node $node
*
* @return int|Node|Node[]|null Replacement node (or special return value)
*/
public function leaveNode(Node $node)
{
// Store a reference to every "use" statement, because we'll need them in a bit.
if ($node instanceof Use_) {
// Store a reference to every "use" statement, because we'll need
// them in a bit.
foreach ($node->uses as $use) {
$this->aliases[strtolower($use->alias)] = $use->name;
$alias = $use->alias ?: \end($use->name->parts);
$this->aliases[\strtolower($alias)] = $use->name;
}
return false;
} elseif ($node instanceof GroupUse) {
// Expand every "use" statement in the group into a full, standalone
// "use" and store 'em with the others.
return NodeTraverser::REMOVE_NODE;
}
// Expand every "use" statement in the group into a full, standalone "use" and store 'em with the others.
if ($node instanceof GroupUse) {
foreach ($node->uses as $use) {
$this->aliases[strtolower($use->alias)] = Name::concat($node->prefix, $use->name, array(
$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 false;
} elseif ($node instanceof Namespace_) {
// Start fresh, since we're done with this namespace.
return NodeTraverser::REMOVE_NODE;
}
// Start fresh, since we're done with this namespace.
if ($node instanceof Namespace_) {
$this->lastNamespace = $node->name;
$this->lastAliases = $this->aliases;
$this->aliases = array();
} else {
foreach ($node as $name => $subNode) {
if ($subNode instanceof Name) {
// Implicitly thunk all aliases.
if ($replacement = $this->findAlias($subNode)) {
$node->$name = $replacement;
}
$this->lastAliases = $this->aliases;
$this->aliases = [];
return;
}
// Do nothing with UseUse; this an entry in the list of uses in the use statement.
if ($node instanceof UseUse) {
return;
}
// For everything else, we'll implicitly thunk all aliases into fully-qualified names.
foreach ($node as $name => $subNode) {
if ($subNode instanceof Name) {
if ($replacement = $this->findAlias($subNode)) {
$node->$name = $replacement;
}
}
return $node;
}
return $node;
}
/**
@@ -111,12 +128,12 @@ class UseStatementPass extends CodeCleanerPass
*/
private function findAlias(Name $name)
{
$that = strtolower($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)));
} elseif (\substr($that, 0, \strlen($alias) + 1) === $alias.'\\') {
return new FullyQualifiedName($prefix->toString().\substr($name, \strlen($alias)));
}
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -13,9 +13,7 @@ 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\Expr\Ternary;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Do_;
@@ -34,18 +32,12 @@ use Psy\Exception\FatalErrorException;
*/
class ValidClassNamePass extends NamespaceAwarePass
{
const CLASS_TYPE = 'class';
const CLASS_TYPE = 'class';
const INTERFACE_TYPE = 'interface';
const TRAIT_TYPE = 'trait';
const TRAIT_TYPE = 'trait';
protected $checkTraits;
private $conditionalScopes = 0;
public function __construct()
{
$this->checkTraits = function_exists('trait_exists');
}
/**
* Validate class, interface and trait definitions.
*
@@ -53,7 +45,9 @@ class ValidClassNamePass extends NamespaceAwarePass
* presence and can validate constant fetches and static calls in class or
* trait methods.
*
* @param Node
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
@@ -61,51 +55,42 @@ class ValidClassNamePass extends NamespaceAwarePass
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);
}
return;
}
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
*
* @return int|Node|Node[]|null Replacement node (or special return value)
*/
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);
return;
}
}
private static function isConditional(Node $node)
private static function isConditional(Node $node): bool
{
return $node instanceof If_ ||
$node instanceof While_ ||
$node instanceof Do_ ||
$node instanceof Switch_;
$node instanceof Switch_ ||
$node instanceof Ternary;
}
/**
@@ -115,7 +100,7 @@ class ValidClassNamePass extends NamespaceAwarePass
*/
protected function validateClassStatement(Class_ $stmt)
{
$this->ensureCanDefine($stmt);
$this->ensureCanDefine($stmt, self::CLASS_TYPE);
if (isset($stmt->extends)) {
$this->ensureClassExists($this->getFullyQualifiedName($stmt->extends), $stmt);
}
@@ -129,7 +114,7 @@ class ValidClassNamePass extends NamespaceAwarePass
*/
protected function validateInterfaceStatement(Interface_ $stmt)
{
$this->ensureCanDefine($stmt);
$this->ensureCanDefine($stmt, self::INTERFACE_TYPE);
$this->ensureInterfacesExist($stmt->extends, $stmt);
}
@@ -140,52 +125,7 @@ class ValidClassNamePass extends NamespaceAwarePass
*/
protected function validateTraitStatement(Trait_ $stmt)
{
$this->ensureCanDefine($stmt);
}
/**
* Validate a `new` expression.
*
* @param New_ $stmt
*/
protected function validateNewExpression(New_ $stmt)
{
// if class name is an expression or an anonymous class, give it a pass for now
if (!$stmt->class instanceof Expr && !$stmt->class instanceof Class_) {
$this->ensureClassExists($this->getFullyQualifiedName($stmt->class), $stmt);
}
}
/**
* Validate a class constant fetch expression's class.
*
* @param ClassConstFetch $stmt
*/
protected function validateClassConstFetchExpression(ClassConstFetch $stmt)
{
// there is no need to check exists for ::class const for php 5.5 or newer
if (strtolower($stmt->name) === 'class'
&& version_compare(PHP_VERSION, '5.5', '>=')) {
return;
}
// if class name is an expression, give it a pass for now
if (!$stmt->class instanceof Expr) {
$this->ensureClassOrInterfaceExists($this->getFullyQualifiedName($stmt->class), $stmt);
}
}
/**
* Validate a class constant fetch expression's class.
*
* @param StaticCall $stmt
*/
protected function validateStaticCallExpression(StaticCall $stmt)
{
// if class name is an expression, give it a pass for now
if (!$stmt->class instanceof Expr) {
$this->ensureMethodExists($this->getFullyQualifiedName($stmt->class), $stmt->name, $stmt);
}
$this->ensureCanDefine($stmt, self::TRAIT_TYPE);
}
/**
@@ -193,10 +133,16 @@ class ValidClassNamePass extends NamespaceAwarePass
*
* @throws FatalErrorException
*
* @param Stmt $stmt
* @param Stmt $stmt
* @param string $scopeType
*/
protected function ensureCanDefine(Stmt $stmt)
protected function ensureCanDefine(Stmt $stmt, string $scopeType = self::CLASS_TYPE)
{
// Anonymous classes don't have a name, and uniqueness shouldn't be enforced.
if ($stmt->name === null) {
return;
}
$name = $this->getFullyQualifiedName($stmt->name);
// check for name collisions
@@ -210,12 +156,12 @@ class ValidClassNamePass extends NamespaceAwarePass
}
if ($errorType !== null) {
throw $this->createError(sprintf('%s named %s already exists', ucfirst($errorType), $name), $stmt);
throw $this->createError(\sprintf('%s named %s already exists', \ucfirst($errorType), $name), $stmt);
}
// Store creation for the rest of this code snippet so we can find local
// issue too
$this->currentScope[strtolower($name)] = $this->getScopeType($stmt);
$this->currentScope[\strtolower($name)] = $scopeType;
}
/**
@@ -226,10 +172,10 @@ class ValidClassNamePass extends NamespaceAwarePass
* @param string $name
* @param Stmt $stmt
*/
protected function ensureClassExists($name, $stmt)
protected function ensureClassExists(string $name, Stmt $stmt)
{
if (!$this->classExists($name)) {
throw $this->createError(sprintf('Class \'%s\' not found', $name), $stmt);
throw $this->createError(\sprintf('Class \'%s\' not found', $name), $stmt);
}
}
@@ -241,10 +187,25 @@ class ValidClassNamePass extends NamespaceAwarePass
* @param string $name
* @param Stmt $stmt
*/
protected function ensureClassOrInterfaceExists($name, $stmt)
protected function ensureClassOrInterfaceExists(string $name, Stmt $stmt)
{
if (!$this->classExists($name) && !$this->interfaceExists($name)) {
throw $this->createError(sprintf('Class \'%s\' not found', $name), $stmt);
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(string $name, Stmt $stmt)
{
if (!$this->classExists($name) && !$this->traitExists($name)) {
throw $this->createError(\sprintf('Class \'%s\' not found', $name), $stmt);
}
}
@@ -257,12 +218,12 @@ class ValidClassNamePass extends NamespaceAwarePass
* @param string $name
* @param Stmt $stmt
*/
protected function ensureMethodExists($class, $name, $stmt)
protected function ensureMethodExists(string $class, string $name, Stmt $stmt)
{
$this->ensureClassExists($class, $stmt);
$this->ensureClassOrTraitExists($class, $stmt);
// let's pretend all calls to self, parent and static are valid
if (in_array(strtolower($class), array('self', 'parent', 'static'))) {
if (\in_array(\strtolower($class), ['self', 'parent', 'static'])) {
return;
}
@@ -276,8 +237,8 @@ class ValidClassNamePass extends NamespaceAwarePass
return;
}
if (!method_exists($class, $name) && !method_exists($class, '__callStatic')) {
throw $this->createError(sprintf('Call to undefined method %s::%s()', $class, $name), $stmt);
if (!\method_exists($class, $name) && !\method_exists($class, '__callStatic')) {
throw $this->createError(\sprintf('Call to undefined method %s::%s()', $class, $name), $stmt);
}
}
@@ -286,16 +247,16 @@ class ValidClassNamePass extends NamespaceAwarePass
*
* @throws FatalErrorException
*
* @param $interfaces
* @param Stmt $stmt
* @param Interface_[] $interfaces
* @param Stmt $stmt
*/
protected function ensureInterfacesExist($interfaces, $stmt)
protected function ensureInterfacesExist(array $interfaces, Stmt $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);
throw $this->createError(\sprintf('Interface \'%s\' not found', $name), $stmt);
}
}
}
@@ -303,11 +264,16 @@ class ValidClassNamePass extends NamespaceAwarePass
/**
* 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
*
* @throws FatalErrorException
*
* @param Stmt $stmt
*
* @return string
*/
protected function getScopeType(Stmt $stmt)
protected function getScopeType(Stmt $stmt): string
{
if ($stmt instanceof Class_) {
return self::CLASS_TYPE;
@@ -316,6 +282,8 @@ class ValidClassNamePass extends NamespaceAwarePass
} elseif ($stmt instanceof Trait_) {
return self::TRAIT_TYPE;
}
throw $this->createError('Unsupported statement type', $stmt);
}
/**
@@ -327,16 +295,16 @@ class ValidClassNamePass extends NamespaceAwarePass
*
* @return bool
*/
protected function classExists($name)
protected function classExists(string $name): bool
{
// Give `self`, `static` and `parent` a pass. This will actually let
// some errors through, since we're not checking whether the keyword is
// being used in a class scope.
if (in_array(strtolower($name), array('self', 'static', 'parent'))) {
if (\in_array(\strtolower($name), ['self', 'static', 'parent'])) {
return true;
}
return class_exists($name) || $this->findInScope($name) === self::CLASS_TYPE;
return \class_exists($name) || $this->findInScope($name) === self::CLASS_TYPE;
}
/**
@@ -346,9 +314,9 @@ class ValidClassNamePass extends NamespaceAwarePass
*
* @return bool
*/
protected function interfaceExists($name)
protected function interfaceExists(string $name): bool
{
return interface_exists($name) || $this->findInScope($name) === self::INTERFACE_TYPE;
return \interface_exists($name) || $this->findInScope($name) === self::INTERFACE_TYPE;
}
/**
@@ -358,9 +326,9 @@ class ValidClassNamePass extends NamespaceAwarePass
*
* @return bool
*/
protected function traitExists($name)
protected function traitExists(string $name): bool
{
return $this->checkTraits && (trait_exists($name) || $this->findInScope($name) === self::TRAIT_TYPE);
return \trait_exists($name) || $this->findInScope($name) === self::TRAIT_TYPE;
}
/**
@@ -370,9 +338,9 @@ class ValidClassNamePass extends NamespaceAwarePass
*
* @return string|null
*/
protected function findInScope($name)
protected function findInScope(string $name)
{
$name = strtolower($name);
$name = \strtolower($name);
if (isset($this->currentScope[$name])) {
return $this->currentScope[$name];
}
@@ -386,8 +354,8 @@ class ValidClassNamePass extends NamespaceAwarePass
*
* @return FatalErrorException
*/
protected function createError($msg, $stmt)
protected function createError(string $msg, Stmt $stmt): FatalErrorException
{
return new FatalErrorException($msg, 0, E_ERROR, null, $stmt->getLine());
return new FatalErrorException($msg, 0, \E_ERROR, null, $stmt->getLine());
}
}

View File

@@ -0,0 +1,117 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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;
/**
* @return Node[]|null Array of nodes
*/
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
*
* @return int|Node|null Replacement node (or special return value)
*/
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

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -12,9 +12,6 @@
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_;
@@ -35,7 +32,11 @@ class ValidFunctionNamePass extends NamespaceAwarePass
/**
* Store newly defined function names on the way in, to allow recursion.
*
* @throws FatalErrorException if a function is redefined in a non-conditional scope
*
* @param Node $node
*
* @return int|Node|null Replacement node (or special return value)
*/
public function enterNode(Node $node)
{
@@ -49,41 +50,26 @@ class ValidFunctionNamePass extends NamespaceAwarePass
// @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());
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;
$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
*
* @return int|Node|Node[]|null Replacement node (or special return value)
*/
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());
}
}
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,6 +11,7 @@
namespace Psy\Command;
use Psy\Exception\RuntimeException;
use Psy\Output\ShellOutput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -30,10 +31,10 @@ class BufferCommand extends Command
{
$this
->setName('buffer')
->setAliases(array('buf'))
->setDefinition(array(
->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'
@@ -46,16 +47,25 @@ HELP
/**
* {@inheritdoc}
*
* @return int 0 if everything went fine, or an exit code
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$buf = $this->getApplication()->getCodeBuffer();
$app = $this->getApplication();
if (!$app instanceof \Psy\Shell) {
throw new RuntimeException('Buffer command requires a \Psy\Shell application');
}
$buf = $app->getCodeBuffer();
if ($input->getOption('clear')) {
$this->getApplication()->resetCodeBuffer();
$app->resetCodeBuffer();
$output->writeln($this->formatLines($buf, 'urgent'), ShellOutput::NUMBER_LINES);
} else {
$output->writeln($this->formatLines($buf), ShellOutput::NUMBER_LINES);
}
return 0;
}
/**
@@ -66,12 +76,12 @@ HELP
*
* @return array Formatted strings
*/
protected function formatLines(array $lines, $type = 'return')
protected function formatLines(array $lines, string $type = 'return'): array
{
$template = sprintf('<%s>%%s</%s>', $type, $type);
$template = \sprintf('<%s>%%s</%s>', $type, $type);
return array_map(function ($line) use ($template) {
return sprintf($template, $line);
return \array_map(function ($line) use ($template) {
return \sprintf($template, $line);
}, $lines);
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -28,7 +28,7 @@ class ClearCommand extends Command
{
$this
->setName('clear')
->setDefinition(array())
->setDefinition([])
->setDescription('Clear the Psy Shell screen.')
->setHelp(
<<<'HELP'
@@ -41,9 +41,13 @@ HELP
/**
* {@inheritdoc}
*
* @return int 0 if everything went fine, or an exit code
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->write(sprintf('%c[2J%c[0;0f', 27, 27));
$output->write(\sprintf('%c[2J%c[0;0f', 27, 27));
return 0;
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -27,14 +27,14 @@ abstract class Command extends BaseCommand
/**
* Sets the application instance for this command.
*
* @param Application $application An Application instance
* @param Application|null $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.');
throw new \InvalidArgumentException('PsySH Commands require an instance of Psy\Shell');
}
return parent::setApplication($application);
@@ -43,13 +43,13 @@ abstract class Command extends BaseCommand
/**
* {@inheritdoc}
*/
public function asText()
public function asText(): string
{
$messages = array(
$messages = [
'<comment>Usage:</comment>',
' ' . $this->getSynopsis(),
' '.$this->getSynopsis(),
'',
);
];
if ($this->getAliases()) {
$messages[] = $this->aliasesAsText();
@@ -65,21 +65,21 @@ abstract class Command extends BaseCommand
if ($help = $this->getProcessedHelp()) {
$messages[] = '<comment>Help:</comment>';
$messages[] = ' ' . str_replace("\n", "\n ", $help) . "\n";
$messages[] = ' '.\str_replace("\n", "\n ", $help)."\n";
}
return implode("\n", $messages);
return \implode("\n", $messages);
}
/**
* {@inheritdoc}
*/
private function getArguments()
private function getArguments(): array
{
$hidden = $this->getHiddenArguments();
return array_filter($this->getNativeDefinition()->getArguments(), function ($argument) use ($hidden) {
return !in_array($argument->getName(), $hidden);
return \array_filter($this->getNativeDefinition()->getArguments(), function ($argument) use ($hidden) {
return !\in_array($argument->getName(), $hidden);
});
}
@@ -88,20 +88,20 @@ abstract class Command extends BaseCommand
*
* @return array
*/
protected function getHiddenArguments()
protected function getHiddenArguments(): array
{
return array('command');
return ['command'];
}
/**
* {@inheritdoc}
*/
private function getOptions()
private function getOptions(): array
{
$hidden = $this->getHiddenOptions();
return array_filter($this->getNativeDefinition()->getOptions(), function ($option) use ($hidden) {
return !in_array($option->getName(), $hidden);
return \array_filter($this->getNativeDefinition()->getOptions(), function ($option) use ($hidden) {
return !\in_array($option->getName(), $hidden);
});
}
@@ -110,9 +110,9 @@ abstract class Command extends BaseCommand
*
* @return array
*/
protected function getHiddenOptions()
protected function getHiddenOptions(): array
{
return array('verbose');
return ['verbose'];
}
/**
@@ -120,9 +120,9 @@ abstract class Command extends BaseCommand
*
* @return string
*/
private function aliasesAsText()
private function aliasesAsText(): string
{
return '<comment>Aliases:</comment> <info>' . implode(', ', $this->getAliases()) . '</info>' . PHP_EOL;
return '<comment>Aliases:</comment> <info>'.\implode(', ', $this->getAliases()).'</info>'.\PHP_EOL;
}
/**
@@ -130,30 +130,30 @@ abstract class Command extends BaseCommand
*
* @return string
*/
private function argumentsAsText()
private function argumentsAsText(): string
{
$max = $this->getMaxWidth();
$messages = array();
$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()));
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());
$description = \str_replace("\n", "\n".\str_pad('', $max + 2, ' '), $argument->getDescription());
$messages[] = sprintf(" <info>%-${max}s</info> %s%s", $argument->getName(), $description, $default);
$messages[] = \sprintf(" <info>%-{$max}s</info> %s%s", $argument->getName(), $description, $default);
}
$messages[] = '';
}
return implode(PHP_EOL, $messages);
return \implode(\PHP_EOL, $messages);
}
/**
@@ -161,30 +161,30 @@ abstract class Command extends BaseCommand
*
* @return string
*/
private function optionsAsText()
private function optionsAsText(): string
{
$max = $this->getMaxWidth();
$messages = array();
$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()));
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());
$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()) : '',
$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
@@ -194,7 +194,7 @@ abstract class Command extends BaseCommand
$messages[] = '';
}
return implode(PHP_EOL, $messages);
return \implode(\PHP_EOL, $messages);
}
/**
@@ -202,21 +202,21 @@ abstract class Command extends BaseCommand
*
* @return int
*/
private function getMaxWidth()
private function getMaxWidth(): int
{
$max = 0;
foreach ($this->getOptions() as $option) {
$nameLength = strlen($option->getName()) + 2;
$nameLength = \strlen($option->getName()) + 2;
if ($option->getShortcut()) {
$nameLength += strlen($option->getShortcut()) + 3;
$nameLength += \strlen($option->getShortcut()) + 3;
}
$max = max($max, $nameLength);
$max = \max($max, $nameLength);
}
foreach ($this->getArguments() as $argument) {
$max = max($max, strlen($argument->getName()));
$max = \max($max, \strlen($argument->getName()));
}
return ++$max;
@@ -229,13 +229,13 @@ abstract class Command extends BaseCommand
*
* @return string
*/
private function formatDefaultValue($default)
private function formatDefaultValue($default): string
{
if (is_array($default) && $default === array_values($default)) {
return sprintf("array('%s')", implode("', '", $default));
if (\is_array($default) && $default === \array_values($default)) {
return \sprintf("['%s']", \implode("', '", $default));
}
return str_replace("\n", '', var_export($default, true));
return \str_replace("\n", '', \var_export($default, true));
}
/**
@@ -247,20 +247,27 @@ abstract class Command extends BaseCommand
*/
protected function getTable(OutputInterface $output)
{
if (!class_exists('Symfony\Component\Console\Helper\Table')) {
if (!\class_exists(Table::class)) {
return $this->getTableHelper();
}
$style = new TableStyle();
$style
->setVerticalBorderChar(' ')
->setHorizontalBorderChar('')
->setCrossingChar('');
// Symfony 4.1 deprecated single-argument style setters.
if (\method_exists($style, 'setVerticalBorderChars')) {
$style->setVerticalBorderChars(' ');
$style->setHorizontalBorderChars('');
$style->setCrossingChars('', '', '', '', '', '', '', '', '');
} else {
$style->setVerticalBorderChar(' ');
$style->setHorizontalBorderChar('');
$style->setCrossingChar('');
}
$table = new Table($output);
return $table
->setRows(array())
->setRows([])
->setStyle($style);
}
@@ -269,12 +276,12 @@ abstract class Command extends BaseCommand
*
* @return TableHelper
*/
protected function getTableHelper()
protected function getTableHelper(): TableHelper
{
$table = $this->getApplication()->getHelperSet()->get('table');
return $table
->setRows(array())
->setRows([])
->setLayout(TableHelper::LAYOUT_BORDERLESS)
->setHorizontalBorderChar('')
->setCrossingChar('');

View File

@@ -0,0 +1,254 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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\Output\ShellOutput;
use Psy\Reflection\ReflectionClassConstant;
use Psy\Reflection\ReflectionConstant_;
use Psy\Reflection\ReflectionLanguageConstruct;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Read the documentation for an object, class, constant, method or property.
*/
class DocCommand extends ReflectingCommand
{
const INHERIT_DOC_TAG = '{@inheritdoc}';
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('doc')
->setAliases(['rtfm', 'man'])
->setDefinition([
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show documentation for superclasses as well as the current class.'),
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}
*
* @return int 0 if everything went fine, or an exit code
*/
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();
if ($output instanceof ShellOutput) {
$output->startPaging();
}
// Maybe include the declaring class
if ($reflector instanceof \ReflectionMethod || $reflector instanceof \ReflectionProperty) {
$output->writeln(SignatureFormatter::format($reflector->getDeclaringClass()));
}
$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);
}
// Implicit --all if the original docblock has an {@inheritdoc} tag.
if ($input->getOption('all') || \stripos($doc, self::INHERIT_DOC_TAG) !== false) {
$parent = $reflector;
foreach ($this->getParentReflectors($reflector) as $parent) {
$output->writeln('');
$output->writeln('---');
$output->writeln('');
// Maybe include the declaring class
if ($parent instanceof \ReflectionMethod || $parent instanceof \ReflectionProperty) {
$output->writeln(SignatureFormatter::format($parent->getDeclaringClass()));
}
$output->writeln(SignatureFormatter::format($parent));
$output->writeln('');
if ($doc = $this->getManualDoc($parent) ?: DocblockFormatter::format($parent)) {
$output->writeln($doc);
}
}
}
if ($output instanceof ShellOutput) {
$output->stopPaging();
}
// Set some magic local variables
$this->setCommandScopeVariables($reflector);
return 0;
}
private function getManualDoc($reflector)
{
switch (\get_class($reflector)) {
case \ReflectionClass::class:
case \ReflectionObject::class:
case \ReflectionFunction::class:
$id = $reflector->name;
break;
case \ReflectionMethod::class:
$id = $reflector->class.'::'.$reflector->name;
break;
case \ReflectionProperty::class:
$id = $reflector->class.'::$'.$reflector->name;
break;
case \ReflectionClassConstant::class:
case ReflectionClassConstant::class:
// @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 ReflectionConstant_::class:
$id = $reflector->name;
break;
default:
return false;
}
return $this->getManualDocById($id);
}
/**
* Get all all parent Reflectors for a given Reflector.
*
* For example, passing a Class, Object or TraitReflector will yield all
* traits and parent classes. Passing a Method or PropertyReflector will
* yield Reflectors for the same-named method or property on all traits and
* parent classes.
*
* @return \Generator a whole bunch of \Reflector instances
*/
private function getParentReflectors($reflector): \Generator
{
$seenClasses = [];
switch (\get_class($reflector)) {
case \ReflectionClass::class:
case \ReflectionObject::class:
foreach ($reflector->getTraits() as $trait) {
if (!\in_array($trait->getName(), $seenClasses)) {
$seenClasses[] = $trait->getName();
yield $trait;
}
}
foreach ($reflector->getInterfaces() as $interface) {
if (!\in_array($interface->getName(), $seenClasses)) {
$seenClasses[] = $interface->getName();
yield $interface;
}
}
while ($reflector = $reflector->getParentClass()) {
yield $reflector;
foreach ($reflector->getTraits() as $trait) {
if (!\in_array($trait->getName(), $seenClasses)) {
$seenClasses[] = $trait->getName();
yield $trait;
}
}
foreach ($reflector->getInterfaces() as $interface) {
if (!\in_array($interface->getName(), $seenClasses)) {
$seenClasses[] = $interface->getName();
yield $interface;
}
}
}
return;
case \ReflectionMethod::class:
foreach ($this->getParentReflectors($reflector->getDeclaringClass()) as $parent) {
if ($parent->hasMethod($reflector->getName())) {
$parentMethod = $parent->getMethod($reflector->getName());
if (!\in_array($parentMethod->getDeclaringClass()->getName(), $seenClasses)) {
$seenClasses[] = $parentMethod->getDeclaringClass()->getName();
yield $parentMethod;
}
}
}
return;
case \ReflectionProperty::class:
foreach ($this->getParentReflectors($reflector->getDeclaringClass()) as $parent) {
if ($parent->hasProperty($reflector->getName())) {
$parentProperty = $parent->getProperty($reflector->getName());
if (!\in_array($parentProperty->getDeclaringClass()->getName(), $seenClasses)) {
$seenClasses[] = $parentProperty->getDeclaringClass()->getName();
yield $parentProperty;
}
}
}
break;
}
}
private function getManualDocById($id)
{
if ($db = $this->getApplication()->getManualDb()) {
$result = $db->query(\sprintf('SELECT doc FROM php_manual WHERE id = %s', $db->quote($id)));
if ($result !== false) {
return $result->fetchColumn(0);
}
}
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,10 +11,9 @@
namespace Psy\Command;
use Psy\Exception\RuntimeException;
use Psy\Input\CodeArgument;
use Psy\VarDumper\Presenter;
use Psy\VarDumper\PresenterAware;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@@ -45,11 +44,11 @@ class DumpCommand extends ReflectingCommand implements PresenterAware
{
$this
->setName('dump')
->setDefinition(array(
new InputArgument('target', InputArgument::REQUIRED, 'A target object or primitive to dump.', null),
new InputOption('depth', '', InputOption::VALUE_REQUIRED, 'Depth to parse', 10),
->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'
@@ -60,46 +59,40 @@ 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}
*
* @return int 0 if everything went fine, or an exit code
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$depth = $input->getOption('depth');
$target = $this->resolveTarget($input->getArgument('target'));
$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)) {
if (\is_object($target)) {
$this->setCommandScopeVariables(new \ReflectionObject($target));
}
return 0;
}
/**
* Resolve dump target name.
* @deprecated Use `resolveCode` instead
*
* @throws RuntimeException if target name does not exist in the current scope
*
* @param string $target
* @param string $name
*
* @return mixed
*/
protected function resolveTarget($target)
protected function resolveTarget(string $name)
{
$matches = array();
if (preg_match(self::SUPERGLOBAL, $target, $matches)) {
if (!array_key_exists($matches[1], $GLOBALS)) {
throw new RuntimeException('Unknown target: ' . $target);
}
@\trigger_error('`resolveTarget` is deprecated; use `resolveCode` instead.', \E_USER_DEPRECATED);
return $GLOBALS[$matches[1]];
} elseif (preg_match(self::INSTANCE, $target, $matches)) {
return $this->getScopeVariable($matches[1]);
} else {
throw new RuntimeException('Unknown target: ' . $target);
}
return $this->resolveCode($name);
}
}

View File

@@ -0,0 +1,192 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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
*
* @return int 0 if everything went fine, or an exit code
*
* @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);
}
return 0;
}
/**
* @param bool $execOption
* @param bool $noExecOption
* @param string|null $filePath
*
* @return bool
*/
private function shouldExecuteFile(bool $execOption, bool $noExecOption, string $filePath = null): bool
{
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(string $fileArgument = null)
{
// If the file argument was a variable, get it from the context
if ($fileArgument !== null &&
$fileArgument !== '' &&
$fileArgument[0] === '$') {
$fileArgument = $this->context->get(\preg_replace('/^\$/', '', $fileArgument));
}
return $fileArgument;
}
/**
* @param string $filePath
* @param bool $shouldRemoveFile
*
* @return string
*
* @throws \UnexpectedValueException if file_get_contents on $filePath returns false instead of a string
*/
private function editFile(string $filePath, bool $shouldRemoveFile): string
{
$escapedFilePath = \escapeshellarg($filePath);
$editor = (isset($_SERVER['EDITOR']) && $_SERVER['EDITOR']) ? $_SERVER['EDITOR'] : 'nano';
$pipes = [];
$proc = \proc_open("{$editor} {$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

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -29,8 +29,8 @@ class ExitCommand extends Command
{
$this
->setName('exit')
->setAliases(array('quit', 'q'))
->setDefinition(array())
->setAliases(['quit', 'q'])
->setDefinition([])
->setDescription('End the current session and return to caller.')
->setHelp(
<<<'HELP'
@@ -44,9 +44,11 @@ HELP
/**
* {@inheritdoc}
*
* @return int 0 if everything went fine, or an exit code
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
throw new BreakException('Goodbye.');
throw new BreakException('Goodbye');
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,6 +11,7 @@
namespace Psy\Command;
use Psy\Output\ShellOutput;
use Symfony\Component\Console\Helper\TableHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -32,10 +33,10 @@ class HelpCommand extends Command
{
$this
->setName('help')
->setAliases(array('?'))
->setDefinition(array(
new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', null),
))
->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.');
}
@@ -45,13 +46,15 @@ class HelpCommand extends Command
*
* @param Command $command
*/
public function setCommand($command)
public function setCommand(Command $command)
{
$this->command = $command;
}
/**
* {@inheritdoc}
*
* @return int 0 if everything went fine, or an exit code
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
@@ -74,25 +77,33 @@ class HelpCommand extends Command
}
if ($command->getAliases()) {
$aliases = sprintf('<comment>Aliases:</comment> %s', implode(', ', $command->getAliases()));
$aliases = \sprintf('<comment>Aliases:</comment> %s', \implode(', ', $command->getAliases()));
} else {
$aliases = '';
}
$table->addRow(array(
sprintf('<info>%s</info>', $name),
$table->addRow([
\sprintf('<info>%s</info>', $name),
$command->getDescription(),
$aliases,
));
]);
}
if ($output instanceof ShellOutput) {
$output->startPaging();
}
$output->startPaging();
if ($table instanceof TableHelper) {
$table->render($output);
} else {
$table->render();
}
$output->stopPaging();
if ($output instanceof ShellOutput) {
$output->stopPaging();
}
}
return 0;
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -27,6 +27,7 @@ use Symfony\Component\Console\Output\OutputInterface;
class HistoryCommand extends Command
{
private $filter;
private $readline;
/**
* {@inheritdoc}
@@ -57,22 +58,22 @@ class HistoryCommand extends Command
$this
->setName('history')
->setAliases(array('hist'))
->setDefinition(array(
new InputOption('show', 's', InputOption::VALUE_REQUIRED, 'Show the given range of lines'),
new InputOption('head', 'H', InputOption::VALUE_REQUIRED, 'Display the first N items.'),
new InputOption('tail', 'T', InputOption::VALUE_REQUIRED, 'Display the last N items.'),
->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('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.'),
))
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'
@@ -89,11 +90,13 @@ HELP
/**
* {@inheritdoc}
*
* @return int 0 if everything went fine, or an exit code
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->validateOnlyOne($input, array('show', 'head', 'tail'));
$this->validateOnlyOne($input, array('save', 'replay', 'clear'));
$this->validateOnlyOne($input, ['show', 'head', 'tail']);
$this->validateOnlyOne($input, ['save', 'replay', 'clear']);
$history = $this->getHistorySlice(
$input->getOption('show'),
@@ -104,16 +107,16 @@ HELP
$this->filter->bind($input);
if ($this->filter->hasFilter()) {
$matches = array();
$highlighted = array();
$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(array(__CLASS__, 'escape'), $chunks);
$glue = sprintf('<urgent>%s</urgent>', self::escape($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);
$highlighted[$i] = \implode($glue, $chunks);
}
} else {
unset($history[$i]);
@@ -122,16 +125,16 @@ HELP
}
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(\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.');
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' : ''));
$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();
@@ -139,11 +142,13 @@ HELP
} else {
$type = $input->getOption('no-numbers') ? 0 : ShellOutput::NUMBER_LINES;
if (!$highlighted) {
$type = $type | ShellOutput::OUTPUT_RAW;
$type = $type | OutputInterface::OUTPUT_RAW;
}
$output->page($highlighted ?: $history, $type);
}
return 0;
}
/**
@@ -153,61 +158,61 @@ HELP
*
* @return array [ start, end ]
*/
private function extractRange($range)
private function extractRange(string $range): array
{
if (preg_match('/^\d+$/', $range)) {
return array($range, $range + 1);
if (\preg_match('/^\d+$/', $range)) {
return [$range, $range + 1];
}
$matches = array();
if ($range !== '..' && preg_match('/^(\d*)\.\.(\d*)$/', $range, $matches)) {
$start = $matches[1] ? intval($matches[1]) : 0;
$end = $matches[2] ? intval($matches[2]) + 1 : PHP_INT_MAX;
$matches = [];
if ($range !== '..' && \preg_match('/^(\d*)\.\.(\d*)$/', $range, $matches)) {
$start = $matches[1] ? (int) $matches[1] : 0;
$end = $matches[2] ? (int) $matches[2] + 1 : \PHP_INT_MAX;
return array($start, $end);
return [$start, $end];
}
throw new \InvalidArgumentException('Unexpected range: ' . $range);
throw new \InvalidArgumentException('Unexpected range: '.$range);
}
/**
* Retrieve a slice of the readline history.
*
* @param string $show
* @param string $head
* @param string $tail
* @param string|null $show
* @param string|null $head
* @param string|null $tail
*
* @return array A slilce of history
* @return array A slice of history
*/
private function getHistorySlice($show, $head, $tail)
private function getHistorySlice($show, $head, $tail): array
{
$history = $this->readline->listHistory();
// don't show the current `history` invocation
array_pop($history);
\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.');
if (!\preg_match('/^\d+$/', $head)) {
throw new \InvalidArgumentException('Please specify an integer argument for --head');
}
$start = 0;
$length = intval($head);
$start = 0;
$length = (int) $head;
} elseif ($tail) {
if (!preg_match('/^\d+$/', $tail)) {
throw new \InvalidArgumentException('Please specify an integer argument for --tail.');
if (!\preg_match('/^\d+$/', $tail)) {
throw new \InvalidArgumentException('Please specify an integer argument for --tail');
}
$start = count($history) - $tail;
$length = intval($tail) + 1;
$start = \count($history) - $tail;
$length = (int) $tail + 1;
} else {
return $history;
}
return array_slice($history, $start, $length, true);
return \array_slice($history, $start, $length, true);
}
/**
@@ -226,7 +231,7 @@ HELP
}
if ($count > 1) {
throw new \InvalidArgumentException('Please specify only one of --' . implode(', --', $options));
throw new \InvalidArgumentException('Please specify only one of --'.\implode(', --', $options));
}
}
@@ -238,7 +243,7 @@ HELP
$this->readline->clearHistory();
}
public static function escape($string)
public static function escape(string $string): string
{
return OutputFormatter::escape($string);
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -16,18 +16,17 @@ use Psy\Command\ListCommand\ClassEnumerator;
use Psy\Command\ListCommand\ConstantEnumerator;
use Psy\Command\ListCommand\FunctionEnumerator;
use Psy\Command\ListCommand\GlobalVariableEnumerator;
use Psy\Command\ListCommand\InterfaceEnumerator;
use Psy\Command\ListCommand\MethodEnumerator;
use Psy\Command\ListCommand\PropertyEnumerator;
use Psy\Command\ListCommand\TraitEnumerator;
use Psy\Command\ListCommand\VariableEnumerator;
use Psy\Exception\RuntimeException;
use Psy\Input\CodeArgument;
use Psy\Input\FilterOptions;
use Psy\Output\ShellOutput;
use Psy\VarDumper\Presenter;
use Psy\VarDumper\PresenterAware;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\TableHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@@ -43,7 +42,7 @@ class ListCommand extends ReflectingCommand implements PresenterAware
/**
* PresenterAware interface.
*
* @param Presenter $manager
* @param Presenter $presenter
*/
public function setPresenter(Presenter $presenter)
{
@@ -59,34 +58,34 @@ class ListCommand extends ReflectingCommand implements PresenterAware
$this
->setName('ls')
->setAliases(array('list', 'dir'))
->setDefinition(array(
new InputArgument('target', InputArgument::OPTIONAL, 'A target class or object to list.', null),
->setAliases(['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('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('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).'),
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('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.'),
))
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'
@@ -106,12 +105,15 @@ e.g.
<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}
*
* @return int 0 if everything went fine, or an exit code
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
@@ -121,13 +123,13 @@ HELP
$method = $input->getOption('long') ? 'writeLong' : 'write';
if ($target = $input->getArgument('target')) {
list($target, $reflector) = $this->getTargetAndReflector($target, true);
list($target, $reflector) = $this->getTargetAndReflector($target);
} else {
$reflector = null;
}
// @todo something cleaner than this :-/
if ($input->getOption('long')) {
if ($output instanceof ShellOutput && $input->getOption('long')) {
$output->startPaging();
}
@@ -135,7 +137,7 @@ HELP
$this->$method($output, $enumerator->enumerate($input, $reflector, $target));
}
if ($input->getOption('long')) {
if ($output instanceof ShellOutput && $input->getOption('long')) {
$output->stopPaging();
}
@@ -143,6 +145,8 @@ HELP
if ($reflector !== null) {
$this->setCommandScopeVariables($reflector);
}
return 0;
}
/**
@@ -153,18 +157,16 @@ HELP
if (!isset($this->enumerators)) {
$mgr = $this->presenter;
$this->enumerators = array(
$this->enumerators = [
new ClassConstantEnumerator($mgr),
new ClassEnumerator($mgr),
new ConstantEnumerator($mgr),
new FunctionEnumerator($mgr),
new GlobalVariableEnumerator($mgr),
new InterfaceEnumerator($mgr),
new PropertyEnumerator($mgr),
new MethodEnumerator($mgr),
new TraitEnumerator($mgr),
new VariableEnumerator($mgr, $this->context),
);
];
}
}
@@ -172,17 +174,17 @@ HELP
* Write the list items to $output.
*
* @param OutputInterface $output
* @param null|array $result List of enumerated items
* @param array $result List of enumerated items
*/
protected function write(OutputInterface $output, array $result = null)
protected function write(OutputInterface $output, array $result)
{
if ($result === null) {
if (\count($result) === 0) {
return;
}
foreach ($result as $label => $items) {
$names = array_map(array($this, 'formatItemName'), $items);
$output->writeln(sprintf('<strong>%s</strong>: %s', $label, implode(', ', $names)));
$names = \array_map([$this, 'formatItemName'], $items);
$output->writeln(\sprintf('<strong>%s</strong>: %s', $label, \implode(', ', $names)));
}
}
@@ -192,11 +194,11 @@ HELP
* Items are listed one per line, and include the item signature.
*
* @param OutputInterface $output
* @param null|array $result List of enumerated items
* @param array $result List of enumerated items
*/
protected function writeLong(OutputInterface $output, array $result = null)
protected function writeLong(OutputInterface $output, array $result)
{
if ($result === null) {
if (\count($result) === 0) {
return;
}
@@ -204,11 +206,11 @@ HELP
foreach ($result as $label => $items) {
$output->writeln('');
$output->writeln(sprintf('<strong>%s:</strong>', $label));
$output->writeln(\sprintf('<strong>%s:</strong>', $label));
$table->setRows(array());
$table->setRows([]);
foreach ($items as $item) {
$table->addRow(array($this->formatItemName($item), $item['value']));
$table->addRow([$this->formatItemName($item), $item['value']]);
}
if ($table instanceof TableHelper) {
@@ -226,9 +228,9 @@ HELP
*
* @return string
*/
private function formatItemName($item)
private function formatItemName(array $item): string
{
return sprintf('<%s>%s</%s>', $item['style'], OutputFormatter::escape($item['name']), $item['style']);
return \sprintf('<%s>%s</%s>', $item['style'], OutputFormatter::escape($item['name']), $item['style']);
}
/**
@@ -242,13 +244,13 @@ HELP
{
if (!$input->getArgument('target')) {
// if no target is passed, there can be no properties or methods
foreach (array('properties', 'methods', 'no-inherit') as $option) {
foreach (['properties', 'methods', 'no-inherit'] as $option) {
if ($input->getOption($option)) {
throw new RuntimeException('--' . $option . ' does not make sense without a specified target.');
throw new RuntimeException('--'.$option.' does not make sense without a specified target');
}
}
foreach (array('globals', 'vars', 'constants', 'functions', 'classes', 'interfaces', 'traits') as $option) {
foreach (['globals', 'vars', 'constants', 'functions', 'classes', 'interfaces', 'traits'] as $option) {
if ($input->getOption($option)) {
return;
}
@@ -258,22 +260,23 @@ HELP
$input->setOption('vars', true);
} else {
// if a target is passed, classes, functions, etc don't make sense
foreach (array('vars', 'globals', 'functions', 'classes', 'interfaces', 'traits') as $option) {
foreach (['vars', 'globals'] as $option) {
if ($input->getOption($option)) {
throw new RuntimeException('--' . $option . ' does not make sense with a specified target.');
throw new RuntimeException('--'.$option.' does not make sense with a specified target');
}
}
foreach (array('constants', 'properties', 'methods') as $option) {
// @todo ensure that 'functions', 'classes', 'interfaces', 'traits' only accept namespace target?
foreach (['constants', 'properties', 'methods', 'functions', 'classes', 'interfaces', 'traits'] as $option) {
if ($input->getOption($option)) {
return;
}
}
// default to --constants --properties --methods if no other options are passed
$input->setOption('constants', true);
$input->setOption('constants', true);
$input->setOption('properties', true);
$input->setOption('methods', true);
$input->setOption('methods', true);
}
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,7 +11,7 @@
namespace Psy\Command\ListCommand;
use Psy\Reflection\ReflectionConstant;
use Psy\Reflection\ReflectionClassConstant;
use Symfony\Component\Console\Input\InputInterface;
/**
@@ -22,33 +22,32 @@ class ClassConstantEnumerator extends Enumerator
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array
{
// only list constants when a Reflector is present.
if ($reflector === null) {
return;
return [];
}
// We can only list constants on actual class (or object) reflectors.
if (!$reflector instanceof \ReflectionClass) {
// @todo handle ReflectionExtension as well
return;
return [];
}
// only list constants if we are specifically asked
if (!$input->getOption('constants')) {
return;
return [];
}
$noInherit = $input->getOption('no-inherit');
$constants = $this->prepareConstants($this->getConstants($reflector, $noInherit));
if (empty($constants)) {
return;
return [];
}
$ret = array();
$ret = [];
$ret[$this->getKindLabel($reflector)] = $constants;
return $ret;
@@ -62,13 +61,13 @@ class ClassConstantEnumerator extends Enumerator
*
* @return array
*/
protected function getConstants(\Reflector $reflector, $noInherit = false)
protected function getConstants(\Reflector $reflector, bool $noInherit = false): array
{
$className = $reflector->getName();
$constants = array();
$constants = [];
foreach ($reflector->getConstants() as $name => $constant) {
$constReflector = new ReflectionConstant($reflector, $name);
$constReflector = ReflectionClassConstant::create($reflector->name, $name);
if ($noInherit && $constReflector->getDeclaringClass()->getName() !== $className) {
continue;
@@ -77,9 +76,7 @@ class ClassConstantEnumerator extends Enumerator
$constants[$name] = $constReflector;
}
// @todo switch to ksort after we drop support for 5.3:
// ksort($constants, SORT_NATURAL | SORT_FLAG_CASE);
uksort($constants, 'strnatcasecmp');
\ksort($constants, \SORT_NATURAL | \SORT_FLAG_CASE);
return $constants;
}
@@ -91,18 +88,18 @@ class ClassConstantEnumerator extends Enumerator
*
* @return array
*/
protected function prepareConstants(array $constants)
protected function prepareConstants(array $constants): array
{
// My kingdom for a generator.
$ret = array();
$ret = [];
foreach ($constants as $name => $constant) {
if ($this->showItem($name)) {
$ret[$name] = array(
$ret[$name] = [
'name' => $name,
'style' => self::IS_CONSTANT,
'value' => $this->presentRef($constant->getValue()),
);
];
}
}
@@ -116,12 +113,10 @@ class ClassConstantEnumerator extends Enumerator
*
* @return string
*/
protected function getKindLabel(\ReflectionClass $reflector)
protected function getKindLabel(\ReflectionClass $reflector): string
{
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,132 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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\ReflectionNamespace;
use Symfony\Component\Console\Input\InputInterface;
/**
* Class Enumerator class.
*/
class ClassEnumerator extends Enumerator
{
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array
{
// if we have a reflector, ensure that it's a namespace reflector
if (($target !== null || $reflector !== null) && !$reflector instanceof ReflectionNamespace) {
return [];
}
$internal = $input->getOption('internal');
$user = $input->getOption('user');
$prefix = $reflector === null ? null : \strtolower($reflector->getName()).'\\';
$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, $prefix));
}
if ($input->getOption('interfaces')) {
$ret = \array_merge($ret, $this->filterClasses('Interfaces', \get_declared_interfaces(), $internal, $user, $prefix));
}
if ($input->getOption('traits')) {
$ret = \array_merge($ret, $this->filterClasses('Traits', \get_declared_traits(), $internal, $user, $prefix));
}
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
* @param string $prefix
*
* @return array
*/
protected function filterClasses(string $key, array $classes, bool $internal, bool $user, string $prefix = null): array
{
$ret = [];
if ($internal) {
$ret['Internal '.$key] = \array_filter($classes, function ($class) use ($prefix) {
if ($prefix !== null && \strpos(\strtolower($class), $prefix) !== 0) {
return false;
}
$refl = new \ReflectionClass($class);
return $refl->isInternal();
});
}
if ($user) {
$ret['User '.$key] = \array_filter($classes, function ($class) use ($prefix) {
if ($prefix !== null && \strpos(\strtolower($class), $prefix) !== 0) {
return false;
}
$refl = new \ReflectionClass($class);
return !$refl->isInternal();
});
}
if (!$user && !$internal) {
$ret[$key] = \array_filter($classes, function ($class) use ($prefix) {
return $prefix === null || \strpos(\strtolower($class), $prefix) === 0;
});
}
return $ret;
}
/**
* Prepare formatted class array.
*
* @param array $classes
*
* @return array
*/
protected function prepareClasses(array $classes): array
{
\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,175 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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\ReflectionNamespace;
use Symfony\Component\Console\Input\InputInterface;
/**
* Constant Enumerator class.
*/
class ConstantEnumerator extends Enumerator
{
// Because `Json` is ugly.
private static $categoryLabels = [
'libxml' => 'libxml',
'openssl' => 'OpenSSL',
'pcre' => 'PCRE',
'sqlite3' => 'SQLite3',
'curl' => 'cURL',
'dom' => 'DOM',
'ftp' => 'FTP',
'gd' => 'GD',
'gmp' => 'GMP',
'iconv' => 'iconv',
'json' => 'JSON',
'ldap' => 'LDAP',
'mbstring' => 'mbstring',
'odbc' => 'ODBC',
'pcntl' => 'PCNTL',
'pgsql' => 'pgsql',
'posix' => 'POSIX',
'mysqli' => 'mysqli',
'soap' => 'SOAP',
'exif' => 'EXIF',
'sysvmsg' => 'sysvmsg',
'xml' => 'XML',
'xsl' => 'XSL',
];
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array
{
// if we have a reflector, ensure that it's a namespace reflector
if (($target !== null || $reflector !== null) && !$reflector instanceof ReflectionNamespace) {
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');
if ($category) {
$category = \strtolower($category);
if ($category === 'internal') {
$internal = true;
$category = null;
} elseif ($category === 'user') {
$user = true;
$category = null;
}
}
$ret = [];
if ($user) {
$ret['User Constants'] = $this->getConstants('user');
}
if ($internal) {
$ret['Internal Constants'] = $this->getConstants('internal');
}
if ($category) {
$caseCategory = \array_key_exists($category, self::$categoryLabels) ? self::$categoryLabels[$category] : \ucfirst($category);
$label = $caseCategory.' Constants';
$ret[$label] = $this->getConstants($category);
}
if (!$user && !$internal && !$category) {
$ret['Constants'] = $this->getConstants();
}
if ($reflector !== null) {
$prefix = \strtolower($reflector->getName()).'\\';
foreach ($ret as $key => $names) {
foreach (\array_keys($names) as $name) {
if (\strpos(\strtolower($name), $prefix) !== 0) {
unset($ret[$key][$name]);
}
}
}
}
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(string $category = null): array
{
if (!$category) {
return \get_defined_constants();
}
$consts = \get_defined_constants(true);
if ($category === 'internal') {
unset($consts['user']);
return \array_merge(...\array_values($consts));
}
foreach ($consts as $key => $value) {
if (\strtolower($key) === $category) {
return $value;
}
}
return [];
}
/**
* Prepare formatted constant array.
*
* @param array $constants
*
* @return array
*/
protected function prepareConstants(array $constants): array
{
// 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

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -23,13 +23,13 @@ use Symfony\Component\Console\Input\InputInterface;
abstract class Enumerator
{
// Output styles
const IS_PUBLIC = 'public';
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';
const IS_PRIVATE = 'private';
const IS_GLOBAL = 'global';
const IS_CONSTANT = 'const';
const IS_CLASS = 'class';
const IS_FUNCTION = 'function';
private $filter;
private $presenter;
@@ -48,13 +48,13 @@ abstract class Enumerator
/**
* Return a list of categorized things with the given input options and target.
*
* @param InputInterface $input
* @param Reflector $reflector
* @param mixed $target
* @param InputInterface $input
* @param \Reflector|null $reflector
* @param mixed $target
*
* @return array
*/
public function enumerate(InputInterface $input, \Reflector $reflector = null, $target = null)
public function enumerate(InputInterface $input, \Reflector $reflector = null, $target = null): array
{
$this->filter->bind($input);
@@ -76,13 +76,13 @@ abstract class Enumerator
* ],
* ]
*
* @param InputInterface $input
* @param Reflector $reflector
* @param mixed $target
* @param InputInterface $input
* @param \Reflector|null $reflector
* @param mixed $target
*
* @return array
*/
abstract protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null);
abstract protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array;
protected function showItem($name)
{

View File

@@ -0,0 +1,116 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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\ReflectionNamespace;
use Symfony\Component\Console\Input\InputInterface;
/**
* Function Enumerator class.
*/
class FunctionEnumerator extends Enumerator
{
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array
{
// if we have a reflector, ensure that it's a namespace reflector
if (($target !== null || $reflector !== null) && !$reflector instanceof ReflectionNamespace) {
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();
}
$prefix = $reflector === null ? null : \strtolower($reflector->getName()).'\\';
$functions = $this->prepareFunctions($functions, $prefix);
if (empty($functions)) {
return [];
}
$ret = [];
$ret[$label] = $functions;
return $ret;
}
/**
* Get defined functions.
*
* Optionally limit functions to "user" or "internal" functions.
*
* @param string|null $type "user" or "internal" (default: both)
*
* @return array
*/
protected function getFunctions(string $type = null): array
{
$funcs = \get_defined_functions();
if ($type) {
return $funcs[$type];
} else {
return \array_merge($funcs['internal'], $funcs['user']);
}
}
/**
* Prepare formatted function array.
*
* @param array $functions
* @param string $prefix
*
* @return array
*/
protected function prepareFunctions(array $functions, string $prefix = null): array
{
\natcasesort($functions);
// My kingdom for a generator.
$ret = [];
foreach ($functions as $name) {
if ($prefix !== null && \strpos(\strtolower($name), $prefix) !== 0) {
continue;
}
if ($this->showItem($name)) {
try {
$ret[$name] = [
'name' => $name,
'style' => self::IS_FUNCTION,
'value' => $this->presentSignature($name),
];
} catch (\Throwable $e) {
// Ignore failures.
}
}
}
return $ret;
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -21,27 +21,27 @@ class GlobalVariableEnumerator extends Enumerator
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array
{
// only list globals when no Reflector is present.
if ($reflector !== null || $target !== null) {
return;
return [];
}
// only list globals if we are specifically asked
if (!$input->getOption('globals')) {
return;
return [];
}
$globals = $this->prepareGlobals($this->getGlobals());
if (empty($globals)) {
return;
return [];
}
return array(
return [
'Global Variables' => $globals,
);
];
}
/**
@@ -49,14 +49,14 @@ class GlobalVariableEnumerator extends Enumerator
*
* @return array
*/
protected function getGlobals()
protected function getGlobals(): array
{
global $GLOBALS;
$names = array_keys($GLOBALS);
natcasesort($names);
$names = \array_keys($GLOBALS);
\natcasesort($names);
$ret = array();
$ret = [];
foreach ($names as $name) {
$ret[$name] = $GLOBALS[$name];
}
@@ -71,19 +71,19 @@ class GlobalVariableEnumerator extends Enumerator
*
* @return array
*/
protected function prepareGlobals($globals)
protected function prepareGlobals(array $globals): array
{
// My kingdom for a generator.
$ret = array();
$ret = [];
foreach ($globals as $name => $value) {
if ($this->showItem($name)) {
$fname = '$' . $name;
$ret[$fname] = array(
$fname = '$'.$name;
$ret[$fname] = [
'name' => $fname,
'style' => self::IS_GLOBAL,
'value' => $this->presentRef($value),
);
];
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -21,22 +21,21 @@ class MethodEnumerator extends Enumerator
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array
{
// only list methods when a Reflector is present.
if ($reflector === null) {
return;
return [];
}
// We can only list methods on actual class (or object) reflectors.
if (!$reflector instanceof \ReflectionClass) {
return;
return [];
}
// only list methods if we are specifically asked
if (!$input->getOption('methods')) {
return;
return [];
}
$showAll = $input->getOption('all');
@@ -44,10 +43,10 @@ class MethodEnumerator extends Enumerator
$methods = $this->prepareMethods($this->getMethods($showAll, $reflector, $noInherit));
if (empty($methods)) {
return;
return [];
}
$ret = array();
$ret = [];
$ret[$this->getKindLabel($reflector)] = $methods;
return $ret;
@@ -62,13 +61,15 @@ class MethodEnumerator extends Enumerator
*
* @return array
*/
protected function getMethods($showAll, \Reflector $reflector, $noInherit = false)
protected function getMethods(bool $showAll, \Reflector $reflector, bool $noInherit = false): array
{
$className = $reflector->getName();
$methods = array();
$methods = [];
foreach ($reflector->getMethods() as $name => $method) {
if ($noInherit && $method->getDeclaringClass()->getName() !== $className) {
// For some reason PHP reflection shows private methods from the parent class, even
// though they're effectively worthless. Let's suppress them here, like --no-inherit
if (($noInherit || $method->isPrivate()) && $method->getDeclaringClass()->getName() !== $className) {
continue;
}
@@ -77,9 +78,7 @@ class MethodEnumerator extends Enumerator
}
}
// @todo switch to ksort after we drop support for 5.3:
// ksort($methods, SORT_NATURAL | SORT_FLAG_CASE);
uksort($methods, 'strnatcasecmp');
\ksort($methods, \SORT_NATURAL | \SORT_FLAG_CASE);
return $methods;
}
@@ -91,18 +90,18 @@ class MethodEnumerator extends Enumerator
*
* @return array
*/
protected function prepareMethods(array $methods)
protected function prepareMethods(array $methods): array
{
// My kingdom for a generator.
$ret = array();
$ret = [];
foreach ($methods as $name => $method) {
if ($this->showItem($name)) {
$ret[$name] = array(
$ret[$name] = [
'name' => $name,
'style' => $this->getVisibilityStyle($method),
'value' => $this->presentSignature($method),
);
];
}
}
@@ -116,11 +115,11 @@ class MethodEnumerator extends Enumerator
*
* @return string
*/
protected function getKindLabel(\ReflectionClass $reflector)
protected function getKindLabel(\ReflectionClass $reflector): string
{
if ($reflector->isInterface()) {
return 'Interface Methods';
} elseif (method_exists($reflector, 'isTrait') && $reflector->isTrait()) {
} elseif (\method_exists($reflector, 'isTrait') && $reflector->isTrait()) {
return 'Trait Methods';
} else {
return 'Class Methods';
@@ -134,7 +133,7 @@ class MethodEnumerator extends Enumerator
*
* @return string
*/
private function getVisibilityStyle(\ReflectionMethod $method)
private function getVisibilityStyle(\ReflectionMethod $method): string
{
if ($method->isPublic()) {
return self::IS_PUBLIC;

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -21,22 +21,22 @@ class PropertyEnumerator extends Enumerator
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array
{
// only list properties when a Reflector is present.
if ($reflector === null) {
return;
return [];
}
// We can only list properties on actual class (or object) reflectors.
if (!$reflector instanceof \ReflectionClass) {
return;
return [];
}
// only list properties if we are specifically asked
if (!$input->getOption('properties')) {
return;
return [];
}
$showAll = $input->getOption('all');
@@ -44,10 +44,10 @@ class PropertyEnumerator extends Enumerator
$properties = $this->prepareProperties($this->getProperties($showAll, $reflector, $noInherit), $target);
if (empty($properties)) {
return;
return [];
}
$ret = array();
$ret = [];
$ret[$this->getKindLabel($reflector)] = $properties;
return $ret;
@@ -62,11 +62,11 @@ class PropertyEnumerator extends Enumerator
*
* @return array
*/
protected function getProperties($showAll, \Reflector $reflector, $noInherit = false)
protected function getProperties(bool $showAll, \Reflector $reflector, bool $noInherit = false): array
{
$className = $reflector->getName();
$properties = array();
$properties = [];
foreach ($reflector->getProperties() as $property) {
if ($noInherit && $property->getDeclaringClass()->getName() !== $className) {
continue;
@@ -77,9 +77,7 @@ class PropertyEnumerator extends Enumerator
}
}
// @todo switch to ksort after we drop support for 5.3:
// ksort($properties, SORT_NATURAL | SORT_FLAG_CASE);
uksort($properties, 'strnatcasecmp');
\ksort($properties, \SORT_NATURAL | \SORT_FLAG_CASE);
return $properties;
}
@@ -91,19 +89,19 @@ class PropertyEnumerator extends Enumerator
*
* @return array
*/
protected function prepareProperties(array $properties, $target = null)
protected function prepareProperties(array $properties, $target = null): array
{
// My kingdom for a generator.
$ret = array();
$ret = [];
foreach ($properties as $name => $property) {
if ($this->showItem($name)) {
$fname = '$' . $name;
$ret[$fname] = array(
$fname = '$'.$name;
$ret[$fname] = [
'name' => $fname,
'style' => $this->getVisibilityStyle($property),
'value' => $this->presentValue($property, $target),
);
];
}
}
@@ -117,11 +115,9 @@ class PropertyEnumerator extends Enumerator
*
* @return string
*/
protected function getKindLabel(\ReflectionClass $reflector)
protected function getKindLabel(\ReflectionClass $reflector): string
{
if ($reflector->isInterface()) {
return 'Interface Properties';
} elseif (method_exists($reflector, 'isTrait') && $reflector->isTrait()) {
if (\method_exists($reflector, 'isTrait') && $reflector->isTrait()) {
return 'Trait Properties';
} else {
return 'Class Properties';
@@ -135,7 +131,7 @@ class PropertyEnumerator extends Enumerator
*
* @return string
*/
private function getVisibilityStyle(\ReflectionProperty $property)
private function getVisibilityStyle(\ReflectionProperty $property): string
{
if ($property->isPublic()) {
return self::IS_PUBLIC;
@@ -154,20 +150,24 @@ class PropertyEnumerator extends Enumerator
*
* @return string
*/
protected function presentValue(\ReflectionProperty $property, $target)
protected function presentValue(\ReflectionProperty $property, $target): string
{
// If $target is a class, trait or interface (try to) get the default
if (!$target) {
return '';
}
// If $target is a class or trait (try to) get the default
// value for the property.
if (!is_object($target)) {
if (!\is_object($target)) {
try {
$refl = new \ReflectionClass($target);
$props = $refl->getDefaultProperties();
if (array_key_exists($property->name, $props)) {
if (\array_key_exists($property->name, $props)) {
$suffix = $property->isStatic() ? '' : ' <aside>(default)</aside>';
return $this->presentRef($props[$property->name]) . $suffix;
return $this->presentRef($props[$property->name]).$suffix;
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
// Well, we gave it a shot.
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -21,9 +21,9 @@ use Symfony\Component\Console\Input\InputInterface;
class VariableEnumerator extends Enumerator
{
// n.b. this array is the order in which special variables will be listed
private static $specialNames = array(
private static $specialNames = [
'_', '_e', '__out', '__function', '__method', '__class', '__namespace', '__file', '__line', '__dir',
);
];
private $context;
@@ -45,28 +45,28 @@ class VariableEnumerator extends Enumerator
/**
* {@inheritdoc}
*/
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null)
protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array
{
// only list variables when no Reflector is present.
if ($reflector !== null || $target !== null) {
return;
return [];
}
// only list variables if we are specifically asked
if (!$input->getOption('vars')) {
return;
return [];
}
$showAll = $input->getOption('all');
$showAll = $input->getOption('all');
$variables = $this->prepareVariables($this->getVariables($showAll));
if (empty($variables)) {
return;
return [];
}
return array(
return [
'Variables' => $variables,
);
];
}
/**
@@ -76,15 +76,12 @@ class VariableEnumerator extends Enumerator
*
* @return array
*/
protected function getVariables($showAll)
protected function getVariables(bool $showAll): array
{
// self:: doesn't work inside closures in PHP 5.3 :-/
$specialNames = self::$specialNames;
$scopeVars = $this->context->getAll();
uksort($scopeVars, function ($a, $b) use ($specialNames) {
$aIndex = array_search($a, $specialNames);
$bIndex = array_search($b, $specialNames);
\uksort($scopeVars, function ($a, $b) {
$aIndex = \array_search($a, self::$specialNames);
$bIndex = \array_search($b, self::$specialNames);
if ($aIndex !== false) {
if ($bIndex !== false) {
@@ -98,12 +95,12 @@ class VariableEnumerator extends Enumerator
return -1;
}
return strnatcasecmp($a, $b);
return \strnatcasecmp($a, $b);
});
$ret = array();
$ret = [];
foreach ($scopeVars as $name => $val) {
if (!$showAll && in_array($name, self::$specialNames)) {
if (!$showAll && \in_array($name, self::$specialNames)) {
continue;
}
@@ -120,18 +117,18 @@ class VariableEnumerator extends Enumerator
*
* @return array
*/
protected function prepareVariables(array $variables)
protected function prepareVariables(array $variables): array
{
// My kingdom for a generator.
$ret = array();
$ret = [];
foreach ($variables as $name => $val) {
if ($this->showItem($name)) {
$fname = '$' . $name;
$ret[$fname] = array(
$fname = '$'.$name;
$ret[$fname] = [
'name' => $fname,
'style' => in_array($name, self::$specialNames) ? self::IS_PRIVATE : self::IS_PUBLIC,
'style' => \in_array($name, self::$specialNames) ? self::IS_PRIVATE : self::IS_PUBLIC,
'value' => $this->presentRef($val),
);
];
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -13,11 +13,12 @@ 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\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@@ -26,8 +27,15 @@ use Symfony\Component\VarDumper\Caster\Caster;
/**
* Parse PHP code and show the abstract syntax tree.
*/
class ParseCommand extends Command implements PresenterAware
class ParseCommand extends Command implements ContextAware, PresenterAware
{
/**
* Context instance (for ContextAware interface).
*
* @var Context
*/
protected $context;
private $presenter;
private $parserFactory;
private $parsers;
@@ -38,11 +46,21 @@ class ParseCommand extends Command implements PresenterAware
public function __construct($name = null)
{
$this->parserFactory = new ParserFactory();
$this->parsers = array();
$this->parsers = [];
parent::__construct($name);
}
/**
* ContextAware interface.
*
* @param Context $context
*/
public function setContext(Context $context)
{
$this->context = $context;
}
/**
* PresenterAware interface.
*
@@ -51,20 +69,20 @@ class ParseCommand extends Command implements PresenterAware
public function setPresenter(Presenter $presenter)
{
$this->presenter = clone $presenter;
$this->presenter->addCasters(array(
'PhpParser\Node' => function (Node $node, array $a) {
$a = array(
Caster::PREFIX_VIRTUAL . 'type' => $node->getType(),
Caster::PREFIX_VIRTUAL . 'attributes' => $node->getAttributes(),
);
$this->presenter->addCasters([
Node::class => 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;
$a[Caster::PREFIX_VIRTUAL.$name] = $node->$name;
}
return $a;
},
));
]);
}
/**
@@ -72,23 +90,17 @@ class ParseCommand extends Command implements PresenterAware
*/
protected function configure()
{
$definition = array(
new CodeArgument('code', InputArgument::REQUIRED, 'PHP code to parse.'),
new InputOption('depth', '', InputOption::VALUE_REQUIRED, 'Depth to parse', 10),
);
if ($this->parserFactory->hasKindsSupport()) {
$msg = 'One of PhpParser\\ParserFactory constants: '
. implode(', ', ParserFactory::getPossibleKinds())
. " (default is based on current interpreter's version)";
$defaultKind = $this->parserFactory->getDefaultKind();
$definition[] = new InputOption('kind', '', InputOption::VALUE_REQUIRED, $msg, $defaultKind);
}
$kindMsg = 'One of PhpParser\\ParserFactory constants: '
.\implode(', ', ParserFactory::getPossibleKinds())
." (default is based on current interpreter's version).";
$this
->setName('parse')
->setDefinition($definition)
->setDefinition([
new CodeArgument('code', CodeArgument::REQUIRED, 'PHP code to parse.'),
new InputOption('depth', '', InputOption::VALUE_REQUIRED, 'Depth to parse.', 10),
new InputOption('kind', '', InputOption::VALUE_REQUIRED, $kindMsg, $this->parserFactory->getDefaultKind()),
])
->setDescription('Parse PHP code and show the abstract syntax tree.')
->setHelp(
<<<'HELP'
@@ -110,14 +122,18 @@ HELP
protected function execute(InputInterface $input, OutputInterface $output)
{
$code = $input->getArgument('code');
if (strpos('<?', $code) === false) {
$code = '<?php ' . $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);
$parserKind = $input->getOption('kind');
$depth = $input->getOption('depth');
$nodes = $this->parse($this->getParser($parserKind), $code);
$output->page($this->presenter->present($nodes, $depth));
$this->context->setReturnValue($nodes);
return 0;
}
/**
@@ -128,17 +144,17 @@ HELP
*
* @return array Statements
*/
private function parse(Parser $parser, $code)
private function parse(Parser $parser, string $code): array
{
try {
return $parser->parse($code);
} catch (\PhpParser\Error $e) {
if (strpos($e->getMessage(), 'unexpected EOF') === false) {
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 . ';');
return $parser->parse($code.';');
}
}
@@ -149,9 +165,9 @@ HELP
*
* @return Parser
*/
private function getParser($kind = null)
private function getParser(string $kind = null): Parser
{
if (!array_key_exists($kind, $this->parsers)) {
if (!\array_key_exists($kind, $this->parsers)) {
$this->parsers[$kind] = $this->parserFactory->createParser($kind);
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -26,7 +26,7 @@ class PsyVersionCommand extends Command
{
$this
->setName('version')
->setDefinition(array())
->setDefinition([])
->setDescription('Show Psy Shell version.')
->setHelp('Show Psy Shell version.');
}
@@ -37,5 +37,7 @@ class PsyVersionCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln($this->getApplication()->getVersion());
return 0;
}
}

View File

@@ -0,0 +1,324 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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\Exception\UnexpectedTargetException;
use Psy\Reflection\ReflectionClassConstant;
use Psy\Reflection\ReflectionConstant_;
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(string $valueName): array
{
$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(string $name, bool $includeFunctions = false): string
{
$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;
}
// Check $name against the current namespace and use statements.
if (self::couldBeClassName($name)) {
try {
$name = $this->resolveCode($name.'::class');
} catch (RuntimeException $e) {
// /shrug
}
}
if ($namespace = $shell->getNamespace()) {
$fullName = $namespace.'\\'.$name;
if (\class_exists($fullName) || \interface_exists($fullName) || ($includeFunctions && \function_exists($fullName))) {
return $fullName;
}
}
return $name;
}
/**
* Check whether a given name could be a class name.
*/
protected function couldBeClassName(string $name): bool
{
// Regex based on https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.class
return \preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*(\\\\[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)*$/', $name) === 1;
}
/**
* 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(string $valueName): array
{
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(string $code)
{
try {
$value = $this->getApplication()->execute($code, true);
} catch (\Throwable $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 UnexpectedTargetException when the code resolves to a non-object value
*
* @param string $code
*
* @return object Variable instance
*/
private function resolveObject(string $code)
{
$value = $this->resolveCode($code);
if (!\is_object($value)) {
throw new UnexpectedTargetException($value, 'Unable to inspect a non-object');
}
return $value;
}
/**
* @deprecated Use `resolveCode` instead
*
* @param string $name
*
* @return mixed Variable instance
*/
protected function resolveInstance(string $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(string $name)
{
return $this->context->get($name);
}
/**
* Get all scope variables from the current shell scope.
*
* @return array
*/
protected function getScopeVariables(): array
{
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::class:
case \ReflectionObject::class:
$vars['__class'] = $reflector->name;
if ($reflector->inNamespace()) {
$vars['__namespace'] = $reflector->getNamespaceName();
}
break;
case \ReflectionMethod::class:
$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::class:
$vars['__function'] = $reflector->name;
if ($reflector->inNamespace()) {
$vars['__namespace'] = $reflector->getNamespaceName();
}
break;
case \ReflectionGenerator::class:
$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::class:
case \ReflectionClassConstant::class:
case ReflectionClassConstant::class:
$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 ReflectionConstant_::class:
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

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,15 +11,12 @@
namespace Psy\Command;
use JakubOnderka\PhpConsoleHighlighter\Highlighter;
use Psy\Configuration;
use Psy\ConsoleColorFactory;
use Psy\Exception\RuntimeException;
use Psy\Exception\UnexpectedTargetException;
use Psy\Formatter\CodeFormatter;
use Psy\Formatter\SignatureFormatter;
use Psy\Output\ShellOutput;
use Psy\Input\CodeArgument;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@@ -29,19 +26,15 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
class ShowCommand extends ReflectingCommand
{
private $colorMode;
private $highlighter;
private $lastException;
private $lastExceptionIndex;
/**
* @param null|string $colorMode (default: null)
* @param string|null $colorMode (deprecated and ignored)
*/
public function __construct($colorMode = null)
{
$this->colorMode = $colorMode ?: Configuration::COLOR_MODE_AUTO;
return parent::__construct();
parent::__construct();
}
/**
@@ -51,10 +44,10 @@ class ShowCommand extends ReflectingCommand
{
$this
->setName('show')
->setDefinition(array(
new InputArgument('value', InputArgument::OPTIONAL, 'Function, class, instance, constant, method or property to show.'),
new InputOption('ex', null, InputOption::VALUE_OPTIONAL, 'Show last exception context. Optionally specify a stack index.', 1),
))
->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
@@ -76,6 +69,8 @@ HELP
/**
* {@inheritdoc}
*
* @return int 0 if everything went fine, or an exit code
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
@@ -95,29 +90,53 @@ HELP
// "no --ex present", because it's the integer 1, "--ex with no value",
// because it's `null`, and "--ex 1", because it's the string "1".
if ($opts['ex'] !== 1) {
if ($input->getArgument('value')) {
throw new \InvalidArgumentException('Too many arguments (supply either "value" or "--ex")');
if ($input->getArgument('target')) {
throw new \InvalidArgumentException('Too many arguments (supply either "target" or "--ex")');
}
return $this->writeExceptionContext($input, $output);
$this->writeExceptionContext($input, $output);
return 0;
}
if ($input->getArgument('value')) {
return $this->writeCodeContext($input, $output);
if ($input->getArgument('target')) {
$this->writeCodeContext($input, $output);
return 0;
}
throw new RuntimeException('Not enough arguments (missing: "value").');
throw new RuntimeException('Not enough arguments (missing: "target")');
}
private function writeCodeContext(InputInterface $input, OutputInterface $output)
{
list($value, $reflector) = $this->getTargetAndReflector($input->getArgument('value'));
try {
list($target, $reflector) = $this->getTargetAndReflector($input->getArgument('target'));
} catch (UnexpectedTargetException $e) {
// If we didn't get a target and Reflector, maybe we got a filename?
$target = $e->getTarget();
if (\is_string($target) && \is_file($target) && $code = @\file_get_contents($target)) {
$file = \realpath($target);
if ($file !== $this->context->get('__file')) {
$this->context->setCommandScopeVariables([
'__file' => $file,
'__dir' => \dirname($file),
]);
}
$output->page(CodeFormatter::formatCode($code));
return;
} else {
throw $e;
}
}
// Set some magic local variables
$this->setCommandScopeVariables($reflector);
try {
$output->page(CodeFormatter::format($reflector, $this->colorMode), ShellOutput::OUTPUT_RAW);
$output->page(CodeFormatter::format($reflector));
} catch (RuntimeException $e) {
$output->writeln(SignatureFormatter::format($reflector));
throw $e;
@@ -140,16 +159,16 @@ HELP
$index = 0;
}
} else {
$index = max(0, intval($input->getOption('ex')) - 1);
$index = \max(0, (int) $input->getOption('ex') - 1);
}
$trace = $exception->getTrace();
array_unshift($trace, array(
\array_unshift($trace, [
'file' => $exception->getFile(),
'line' => $exception->getLine(),
));
]);
if ($index >= count($trace)) {
if ($index >= \count($trace)) {
$index = 0;
}
@@ -169,25 +188,25 @@ HELP
$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).',
$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)
\count($trace)
));
}
private function replaceCwd($file)
private function replaceCwd(string $file): string
{
if ($cwd = getcwd()) {
$cwd = rtrim($cwd, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if ($cwd = \getcwd()) {
$cwd = \rtrim($cwd, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR;
}
if ($cwd === false) {
return $file;
} else {
return preg_replace('/^' . preg_quote($cwd, '/') . '/', '', $file);
return \preg_replace('/^'.\preg_quote($cwd, '/').'/', '', $file);
}
}
@@ -208,39 +227,49 @@ HELP
$line = $trace[$index]['line'];
}
if (is_file($file)) {
$code = @file_get_contents($file);
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);
}
$startLine = \max($line - 5, 0);
$endLine = $line + 5;
private function getHighlighter()
{
if (!$this->highlighter) {
$factory = new ConsoleColorFactory($this->colorMode);
$this->highlighter = new Highlighter($factory->getConsoleColor());
}
return $this->highlighter;
$output->write(CodeFormatter::formatCode($code, $startLine, $endLine, $line), false);
}
private function setCommandScopeVariablesFromContext(array $context)
{
$vars = array();
$vars = [];
// @todo __namespace?
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 (\Throwable $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 (\Throwable $e) {
// oh well
}
}
if (isset($context['file'])) {
@@ -251,22 +280,22 @@ HELP
$line = $context['line'];
}
if (is_file($file)) {
if (\is_file($file)) {
$vars['__file'] = $file;
if (isset($line)) {
$vars['__line'] = $line;
}
$vars['__dir'] = dirname($file);
$vars['__dir'] = \dirname($file);
}
}
$this->context->setCommandScopeVariables($vars);
}
private function extractEvalFileAndLine($file)
private function extractEvalFileAndLine(string $file)
{
if (preg_match('/(.*)\\((\\d+)\\) : eval\\(\\)\'d code$/', $file, $matches)) {
return array($matches[1], $matches[2]);
if (\preg_match('/(.*)\\((\\d+)\\) : eval\\(\\)\'d code$/', $file, $matches)) {
return [$matches[1], $matches[2]];
}
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -17,7 +17,6 @@ use Psy\Input\CodeArgument;
use Psy\ParserFactory;
use Psy\Readline\Readline;
use Psy\Sudo\SudoVisitor;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -64,9 +63,9 @@ class SudoCommand extends Command
{
$this
->setName('sudo')
->setDefinition(array(
new CodeArgument('code', InputArgument::REQUIRED, 'Code to execute.'),
))
->setDefinition([
new CodeArgument('code', CodeArgument::REQUIRED, 'Code to execute.'),
])
->setDescription('Evaluate PHP code, bypassing visibility restrictions.')
->setHelp(
<<<'HELP'
@@ -96,6 +95,8 @@ HELP
/**
* {@inheritdoc}
*
* @return int 0 if everything went fine, or an exit code
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
@@ -104,20 +105,23 @@ HELP
// special case for !!
if ($code === '!!') {
$history = $this->readline->listHistory();
if (count($history) < 2) {
if (\count($history) < 2) {
throw new \InvalidArgumentException('No previous command to replay');
}
$code = $history[count($history) - 2];
$code = $history[\count($history) - 2];
}
if (strpos('<?', $code) === false) {
$code = '<?php ' . $code;
if (\strpos($code, '<?') === false) {
$code = '<?php '.$code;
}
$nodes = $this->traverser->traverse($this->parse($code));
$sudoCode = $this->printer->prettyPrint($nodes);
$this->getApplication()->addInput($sudoCode, true);
$shell = $this->getApplication();
$shell->addCode($sudoCode, !$shell->hasCode());
return 0;
}
/**
@@ -127,17 +131,17 @@ HELP
*
* @return array Statements
*/
private function parse($code)
private function parse(string $code): array
{
try {
return $this->parser->parse($code);
} catch (\PhpParser\Error $e) {
if (strpos($e->getMessage(), 'unexpected EOF') === false) {
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 . ';');
return $this->parser->parse($code.';');
}
}
}

View File

@@ -0,0 +1,167 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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\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\Exception\ThrowUpException;
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
{
private $parser;
private $printer;
/**
* {@inheritdoc}
*/
public function __construct($name = null)
{
$parserFactory = new ParserFactory();
$this->parser = $parserFactory->createParser();
$this->printer = new Printer();
parent::__construct($name);
}
/**
* @deprecated throwUp no longer needs to be ContextAware
*
* @param Context $context
*/
public function setContext(Context $context)
{
// Do nothing
}
/**
* {@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}
*
* @return int 0 if everything went fine, or an exit code
*
* @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 New_(new FullyQualifiedName(ThrowUpException::class), $args));
$throwCode = $this->printer->prettyPrint([$throwStmt]);
$shell = $this->getApplication();
$shell->addCode($throwCode, !$shell->hasCode());
return 0;
}
/**
* 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(string $code = null): array
{
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::class), $args)];
}
return $args;
}
/**
* Lex and parse a string of code into statements.
*
* @param string $code
*
* @return array Statements
*/
private function parse(string $code): array
{
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,199 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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}
*
* @return int 0 if everything went fine, or an exit code
*/
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));
}
return 0;
}
/**
* 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(string $code): string
{
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(string $code): array
{
$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,148 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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;
use Psy\Command\TimeitCommand;
/**
* 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}
*
* @return Node[]|null Array of nodes
*/
public function beforeTraverse(array $nodes)
{
$this->functionDepth = 0;
}
/**
* {@inheritdoc}
*
* @return int|Node|null Replacement node (or special return value)
*/
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}
*
* @return int|Node|Node[]|null Replacement node (or special return value)
*/
public function leaveNode(Node $node)
{
if ($node instanceof FunctionLike) {
$this->functionDepth--;
}
}
/**
* {@inheritdoc}
*
* @return Node[]|null Array of nodes
*/
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(): StaticCall
{
return new StaticCall(new FullyQualifiedName(TimeitCommand::class), '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): StaticCall
{
if ($arg === null) {
$arg = NoReturnValue::create();
}
return new StaticCall(new FullyQualifiedName(TimeitCommand::class), '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(Node $expr, array $attrs = [])
{
return \class_exists(Expression::class) ? new Expression($expr, $attrs) : $expr;
}
}

View File

@@ -0,0 +1,99 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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\TraceFormatter;
use Psy\Input\FilterOptions;
use Psy\Output\ShellOutput;
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}
*
* @return int 0 if everything went fine, or an exit code
*/
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);
return 0;
}
/**
* Get a backtrace for an exception or error.
*
* Optionally limit the number of rows to include with $count, and exclude
* Psy from the trace.
*
* @param \Throwable $e The exception or error with a backtrace
* @param int $count (default: PHP_INT_MAX)
* @param bool $includePsy (default: true)
*
* @return array Formatted stacktrace lines
*/
protected function getBacktrace(\Throwable $e, int $count = null, bool $includePsy = true): array
{
return TraceFormatter::formatTrace($e, $this->filter, $count, $includePsy);
}
}

View File

@@ -0,0 +1,161 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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\CodeFormatter;
use Psy\Output\ShellOutput;
use Psy\Shell;
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 $backtrace;
/**
* @param string|null $colorMode (deprecated and ignored)
*/
public function __construct($colorMode = null)
{
$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'),
new InputOption('file', 'f|a', InputOption::VALUE_NONE, 'Show the full source for the current file.'),
])
->setDescription('Show where you are in the code.')
->setHelp(
<<<'HELP'
Show where you are in the code.
Optionally, include the number of lines before and after you want to display,
or --file for the whole file.
e.g.
<return>> whereami </return>
<return>> whereami -n10</return>
<return>> whereami --file</return>
HELP
);
}
/**
* Obtains the correct stack frame in the full backtrace.
*
* @return array
*/
protected function trace(): array
{
foreach (\array_reverse($this->backtrace) as $stackFrame) {
if ($this->isDebugCall($stackFrame)) {
return $stackFrame;
}
}
return \end($this->backtrace);
}
private static function isDebugCall(array $stackFrame): bool
{
$class = isset($stackFrame['class']) ? $stackFrame['class'] : null;
$function = isset($stackFrame['function']) ? $stackFrame['function'] : null;
return ($class === null && $function === 'Psy\\debug') ||
($class === Shell::class && \in_array($function, ['__construct', 'debug']));
}
/**
* Determine the file and line based on the specific backtrace.
*
* @return array
*/
protected function fileInfo(): array
{
$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}
*
* @return int 0 if everything went fine, or an exit code
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$info = $this->fileInfo();
$num = $input->getOption('num');
$lineNum = $info['line'];
$startLine = \max($lineNum - $num, 1);
$endLine = $lineNum + $num;
$code = \file_get_contents($info['file']);
if ($input->getOption('file')) {
$startLine = 1;
$endLine = null;
}
if ($output instanceof ShellOutput) {
$output->startPaging();
}
$output->writeln(\sprintf('From <info>%s:%s</info>:', $this->replaceCwd($info['file']), $lineNum));
$output->write(CodeFormatter::formatCode($code, $startLine, $endLine, $lineNum), false);
if ($output instanceof ShellOutput) {
$output->stopPaging();
}
return 0;
}
/**
* Replace the given directory from the start of a filepath.
*
* @param string $file
*
* @return string
*/
private function replaceCwd(string $file): string
{
$cwd = \getcwd();
if ($cwd === false) {
return $file;
}
$cwd = \rtrim($cwd, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR;
return \preg_replace('/^'.\preg_quote($cwd, '/').'/', '', $file);
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -51,15 +51,15 @@ class WtfCommand extends TraceCommand implements ContextAware
$this
->setName('wtf')
->setAliases(array('last-exception', 'wtf?'))
->setDefinition(array(
new InputArgument('incredulity', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Number of lines to show'),
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show entire backtrace.'),
->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'
@@ -74,38 +74,44 @@ e.g.
To see the entire backtrace, pass the -a/--all flag:
e.g.
<return>>>> wtf -v</return>
<return>>>> wtf -a</return>
HELP
);
}
/**
* {@inheritdoc}
*
* @return int 0 if everything went fine, or an exit code
*/
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 "!".');
$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));
$count = $input->getOption('all') ? \PHP_INT_MAX : \max(3, \pow(2, \strlen($incredulity) + 1));
$shell = $this->getApplication();
$output->startPaging();
if ($output instanceof ShellOutput) {
$output->startPaging();
}
do {
$traceCount = count($exception->getTrace());
$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;
if ($traceCount < \max($count * 1.2, $count + 2)) {
$showLines = \PHP_INT_MAX;
}
$trace = $this->getBacktrace($exception, $showLines);
$moreLines = $traceCount - count($trace);
$moreLines = $traceCount - \count($trace);
$output->writeln($shell->formatException($exception));
$output->writeln('--');
@@ -113,13 +119,18 @@ HELP
$output->writeln('');
if ($moreLines > 0) {
$output->writeln(sprintf(
$output->writeln(\sprintf(
'<aside>Use <return>wtf -a</return> to see %d more lines</aside>',
$moreLines
));
$output->writeln('');
}
} while ($exception = $exception->getPrevious());
$output->stopPaging();
if ($output instanceof ShellOutput) {
$output->stopPaging();
}
return 0;
}
}

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

@@ -0,0 +1,447 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy;
/**
* A Psy Shell configuration path helper.
*/
class ConfigPaths
{
private $configDir;
private $dataDir;
private $runtimeDir;
private $env;
/**
* ConfigPaths constructor.
*
* Optionally provide `configDir`, `dataDir` and `runtimeDir` overrides.
*
* @see self::overrideDirs
*
* @param string[] $overrides Directory overrides
* @param EnvInterface $env
*/
public function __construct(array $overrides = [], EnvInterface $env = null)
{
$this->overrideDirs($overrides);
$this->env = $env ?: new SuperglobalsEnv();
}
/**
* Provide `configDir`, `dataDir` and `runtimeDir` overrides.
*
* If a key is set but empty, the override will be removed. If it is not set
* at all, any existing override will persist.
*
* @param string[] $overrides Directory overrides
*/
public function overrideDirs(array $overrides)
{
if (\array_key_exists('configDir', $overrides)) {
$this->configDir = $overrides['configDir'] ?: null;
}
if (\array_key_exists('dataDir', $overrides)) {
$this->dataDir = $overrides['dataDir'] ?: null;
}
if (\array_key_exists('runtimeDir', $overrides)) {
$this->runtimeDir = $overrides['runtimeDir'] ?: null;
}
}
/**
* Get the current home directory.
*
* @return string|null
*/
public function homeDir()
{
if ($homeDir = $this->getEnv('HOME') ?: $this->windowsHomeDir()) {
return \strtr($homeDir, '\\', '/');
}
return null;
}
private function windowsHomeDir()
{
if (\defined('PHP_WINDOWS_VERSION_MAJOR')) {
$homeDrive = $this->getEnv('HOMEDRIVE');
$homePath = $this->getEnv('HOMEPATH');
if ($homeDrive && $homePath) {
return $homeDrive.'/'.$homePath;
}
}
return null;
}
private function homeConfigDir()
{
if ($homeConfigDir = $this->getEnv('XDG_CONFIG_HOME')) {
return $homeConfigDir;
}
$homeDir = $this->homeDir();
return $homeDir === '/' ? $homeDir.'.config' : $homeDir.'/.config';
}
/**
* 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 function configDirs(): array
{
if ($this->configDir !== null) {
return [$this->configDir];
}
$configDirs = $this->getEnvArray('XDG_CONFIG_DIRS') ?: ['/etc/xdg'];
return $this->allDirNames(\array_merge([$this->homeConfigDir()], $configDirs));
}
/**
* @deprecated
*/
public static function getConfigDirs(): array
{
return (new self())->configDirs();
}
/**
* 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
*
* @deprecated
*
* @return string[]
*/
public static function getHomeConfigDirs(): array
{
// Not quite the same, but this is deprecated anyway /shrug
return self::getConfigDirs();
}
/**
* 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::homeConfigDir
*
* @return string
*/
public function currentConfigDir(): string
{
if ($this->configDir !== null) {
return $this->configDir;
}
$configDirs = $this->allDirNames([$this->homeConfigDir()]);
foreach ($configDirs as $configDir) {
if (@\is_dir($configDir)) {
return $configDir;
}
}
return $configDirs[0];
}
/**
* @deprecated
*/
public static function getCurrentConfigDir(): string
{
return (new self())->currentConfigDir();
}
/**
* Find real config files in config directories.
*
* @param string[] $names Config file names
*
* @return string[]
*/
public function configFiles(array $names): array
{
return $this->allRealFiles($this->configDirs(), $names);
}
/**
* @deprecated
*/
public static function getConfigFiles(array $names, $configDir = null): array
{
return (new self(['configDir' => $configDir]))->configFiles($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 function dataDirs(): array
{
if ($this->dataDir !== null) {
return [$this->dataDir];
}
$homeDataDir = $this->getEnv('XDG_DATA_HOME') ?: $this->homeDir().'/.local/share';
$dataDirs = $this->getEnvArray('XDG_DATA_DIRS') ?: ['/usr/local/share', '/usr/share'];
return $this->allDirNames(\array_merge([$homeDataDir], $dataDirs));
}
/**
* @deprecated
*/
public static function getDataDirs(): array
{
return (new self())->dataDirs();
}
/**
* Find real data files in config directories.
*
* @param string[] $names Config file names
*
* @return string[]
*/
public function dataFiles(array $names): array
{
return $this->allRealFiles($this->dataDirs(), $names);
}
/**
* @deprecated
*/
public static function getDataFiles(array $names, $dataDir = null): array
{
return (new self(['dataDir' => $dataDir]))->dataFiles($names);
}
/**
* Get a runtime directory.
*
* Defaults to `/psysh` inside the system's temp dir.
*
* @return string
*/
public function runtimeDir(): string
{
if ($this->runtimeDir !== null) {
return $this->runtimeDir;
}
// Fallback to a boring old folder in the system temp dir.
$runtimeDir = $this->getEnv('XDG_RUNTIME_DIR') ?: \sys_get_temp_dir();
return \strtr($runtimeDir, '\\', '/').'/psysh';
}
/**
* @deprecated
*/
public static function getRuntimeDir(): string
{
return (new self())->runtimeDir();
}
/**
* Get a list of directories in PATH.
*
* If $PATH is unset/empty it defaults to '/usr/sbin:/usr/bin:/sbin:/bin'.
*
* @return string[]
*/
public function pathDirs(): array
{
return $this->getEnvArray('PATH') ?: ['/usr/sbin', '/usr/bin', '/sbin', '/bin'];
}
/**
* Locate a command (an executable) in $PATH.
*
* Behaves like 'command -v COMMAND' or 'which COMMAND'.
* If $PATH is unset/empty it defaults to '/usr/sbin:/usr/bin:/sbin:/bin'.
*
* @param string $command the executable to locate
*
* @return string
*/
public function which($command)
{
foreach ($this->pathDirs() as $path) {
$fullpath = $path.\DIRECTORY_SEPARATOR.$command;
if (@\is_file($fullpath) && @\is_executable($fullpath)) {
return $fullpath;
}
}
return null;
}
/**
* Get all PsySH directory name candidates given a list of base directories.
*
* This expects that XDG-compatible directory paths will be passed in.
* `psysh` will be added to each of $baseDirs, and we'll throw in `~/.psysh`
* and a couple of Windows-friendly paths as well.
*
* @param string[] $baseDirs base directory paths
*
* @return string[]
*/
private function allDirNames(array $baseDirs): array
{
$dirs = \array_map(function ($dir) {
return \strtr($dir, '\\', '/').'/psysh';
}, $baseDirs);
// Add ~/.psysh
if ($home = $this->getEnv('HOME')) {
$dirs[] = \strtr($home, '\\', '/').'/.psysh';
}
// Add some Windows specific ones :)
if (\defined('PHP_WINDOWS_VERSION_MAJOR')) {
if ($appData = $this->getEnv('APPDATA')) {
// AppData gets preference
\array_unshift($dirs, \strtr($appData, '\\', '/').'/PsySH');
}
if ($windowsHomeDir = $this->windowsHomeDir()) {
$dir = \strtr($windowsHomeDir, '\\', '/').'/.psysh';
if (!\in_array($dir, $dirs)) {
$dirs[] = $dir;
}
}
}
return $dirs;
}
/**
* Given a list of directories, and a list of filenames, find the ones that
* are real files.
*
* @return string[]
*/
private function allRealFiles(array $dirNames, array $fileNames): array
{
$files = [];
foreach ($dirNames as $dir) {
foreach ($fileNames as $name) {
$file = $dir.'/'.$name;
if (@\is_file($file)) {
$files[] = $file;
}
}
}
return $files;
}
/**
* Ensure that $dir exists and is writable.
*
* Generates E_USER_NOTICE error if the directory is not writable or creatable.
*
* @param string $dir
*
* @return bool False if directory exists but is not writeable, or cannot be created
*/
public static function ensureDir(string $dir): bool
{
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 directory %s is not allowed.', $dir), \E_USER_NOTICE);
return false;
}
return true;
}
/**
* 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(string $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;
}
if (!self::ensureDir(\dirname($file))) {
return false;
}
\touch($file);
return $file;
}
private function getEnv($key)
{
return $this->env->get($key);
}
private function getEnvArray($key)
{
if ($value = $this->getEnv($key)) {
return \explode(\PATH_SEPARATOR, $value);
}
return null;
}
}

1898
vendor/psy/psysh/src/Configuration.php vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -19,31 +19,32 @@ namespace Psy;
*/
class Context
{
private static $specialNames = array('_', '_e', '__out', '__psysh__', 'this');
private static $specialNames = ['_', '_e', '__out', '__psysh__', 'this'];
// Whitelist a very limited number of command-scope magic variable names.
// Include a very limited number of command-scope magic variable names.
// This might be a bad idea, but future me can sort it out.
private static $commandScopeNames = array(
private static $commandScopeNames = [
'__function', '__method', '__class', '__namespace', '__file', '__line', '__dir',
);
];
private $scopeVariables = array();
private $commandScopeVariables = array();
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
* @throws \InvalidArgumentException If the variable is not found in the current context
*
* @param string $name
*
* @return mixed
*/
public function get($name)
public function get(string $name)
{
switch ($name) {
case '_':
@@ -74,19 +75,19 @@ class Context
case '__file':
case '__line':
case '__dir':
if (array_key_exists($name, $this->commandScopeVariables)) {
if (\array_key_exists($name, $this->commandScopeVariables)) {
return $this->commandScopeVariables[$name];
}
break;
default:
if (array_key_exists($name, $this->scopeVariables)) {
if (\array_key_exists($name, $this->scopeVariables)) {
return $this->scopeVariables[$name];
}
break;
}
throw new \InvalidArgumentException('Unknown variable: $' . $name);
throw new \InvalidArgumentException('Unknown variable: $'.$name);
}
/**
@@ -94,9 +95,9 @@ class Context
*
* @return array
*/
public function getAll()
public function getAll(): array
{
return array_merge($this->scopeVariables, $this->getSpecialVariables());
return \array_merge($this->scopeVariables, $this->getSpecialVariables());
}
/**
@@ -104,11 +105,11 @@ class Context
*
* @return array
*/
public function getSpecialVariables()
public function getSpecialVariables(): array
{
$vars = array(
$vars = [
'_' => $this->returnValue,
);
];
if (isset($this->lastException)) {
$vars['_e'] = $this->lastException;
@@ -122,7 +123,7 @@ class Context
$vars['this'] = $this->boundObject;
}
return array_merge($vars, $this->commandScopeVariables);
return \array_merge($vars, $this->commandScopeVariables);
}
/**
@@ -167,21 +168,21 @@ class Context
}
/**
* Set the most recent Exception.
* Set the most recent Exception or Error.
*
* @param \Exception $e
* @param \Throwable $e
*/
public function setLastException(\Exception $e)
public function setLastException(\Throwable $e)
{
$this->lastException = $e;
}
/**
* Get the most recent Exception.
* Get the most recent Exception or Error.
*
* @throws InvalidArgumentException If no Exception has been caught
* @throws \InvalidArgumentException If no Exception has been caught
*
* @return null|Exception
* @return \Throwable|null
*/
public function getLastException()
{
@@ -197,7 +198,7 @@ class Context
*
* @param string $lastStdout
*/
public function setLastStdout($lastStdout)
public function setLastStdout(string $lastStdout)
{
$this->lastStdout = $lastStdout;
}
@@ -205,9 +206,9 @@ class Context
/**
* Get the most recent output from evaluated code.
*
* @throws InvalidArgumentException If no output has happened yet
* @throws \InvalidArgumentException If no output has happened yet
*
* @return null|string
* @return string|null
*/
public function getLastStdout()
{
@@ -221,11 +222,14 @@ class Context
/**
* 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->boundObject = \is_object($boundObject) ? $boundObject : null;
$this->boundClass = null;
}
/**
@@ -238,6 +242,29 @@ class Context
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.
*
@@ -245,10 +272,10 @@ class Context
*/
public function setCommandScopeVariables(array $commandScopeVariables)
{
$vars = array();
$vars = [];
foreach ($commandScopeVariables as $key => $value) {
// kind of type check
if (is_scalar($value) && in_array($key, self::$commandScopeNames)) {
if (\is_scalar($value) && \in_array($key, self::$commandScopeNames)) {
$vars[$key] = $value;
}
}
@@ -261,7 +288,7 @@ class Context
*
* @return array
*/
public function getCommandScopeVariables()
public function getCommandScopeVariables(): array
{
return $this->commandScopeVariables;
}
@@ -274,9 +301,9 @@ class Context
*
* @return array Array of unused variable names
*/
public function getUnusedCommandScopeVariableNames()
public function getUnusedCommandScopeVariableNames(): array
{
return array_diff(self::$commandScopeNames, array_keys($this->commandScopeVariables));
return \array_diff(self::$commandScopeNames, \array_keys($this->commandScopeVariables));
}
/**
@@ -286,8 +313,8 @@ class Context
*
* @return bool
*/
public static function isSpecialVariableName($name)
public static function isSpecialVariableName(string $name): bool
{
return in_array($name, self::$specialNames) || in_array($name, self::$commandScopeNames);
return \in_array($name, self::$specialNames) || \in_array($name, self::$commandScopeNames);
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.

25
vendor/psy/psysh/src/EnvInterface.php vendored Normal file
View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy;
/**
* Abstraction around environment variables.
*/
interface EnvInterface
{
/**
* Get an environment variable by name.
*
* @return string|null
*/
public function get(string $key);
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -21,10 +21,10 @@ class BreakException extends \Exception implements Exception
/**
* {@inheritdoc}
*/
public function __construct($message = '', $code = 0, \Exception $previous = null)
public function __construct($message = '', $code = 0, \Throwable $previous = null)
{
$this->rawMessage = $message;
parent::__construct(sprintf('Exit: %s', $message), $code, $previous);
parent::__construct(\sprintf('Exit: %s', $message), $code, $previous);
}
/**
@@ -32,7 +32,7 @@ class BreakException extends \Exception implements Exception
*
* @return string
*/
public function getRawMessage()
public function getRawMessage(): string
{
return $this->rawMessage;
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -21,39 +21,53 @@ class ErrorException extends \ErrorException implements Exception
/**
* 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)
* @param string $message (default: "")
* @param int $code (default: 0)
* @param int $severity (default: 1)
* @param string|null $filename (default: null)
* @param int|null $lineno (default: null)
* @param \Throwable|null $previous (default: null)
*/
public function __construct($message = '', $code = 0, $severity = 1, $filename = null, $lineno = null, $previous = null)
public function __construct($message = '', $code = 0, $severity = 1, $filename = null, $lineno = null, \Throwable $previous = null)
{
$this->rawMessage = $message;
if (!empty($filename) && preg_match('{Psy[/\\\\]ExecutionLoop}', $filename)) {
if (!empty($filename) && \preg_match('{Psy[/\\\\]ExecutionLoop}', $filename)) {
$filename = '';
}
switch ($severity) {
case E_WARNING:
case E_CORE_WARNING:
case E_COMPILE_WARNING:
case E_USER_WARNING:
$type = 'warning';
break;
case E_STRICT:
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';
$type = 'Error';
break;
}
$message = sprintf('PHP %s: %s%s on line %d', $type, $message, $filename ? ' in ' . $filename : '', $lineno);
$message = \sprintf('PHP %s: %s%s on line %d', $type, $message, $filename ? ' in '.$filename : '', $lineno);
parent::__construct($message, $code, $severity, $filename, $lineno, $previous);
}
@@ -62,7 +76,7 @@ class ErrorException extends \ErrorException implements Exception
*
* @return string
*/
public function getRawMessage()
public function getRawMessage(): string
{
return $this->rawMessage;
}
@@ -72,9 +86,9 @@ class ErrorException extends \ErrorException implements Exception
*
* This allows us to:
*
* set_error_handler(array('Psy\Exception\ErrorException', 'throwException'));
* set_error_handler([ErrorException::class, 'throwException']);
*
* @throws ErrorException
* @throws self
*
* @param int $errno Error type
* @param string $errstr Message
@@ -89,11 +103,13 @@ class ErrorException extends \ErrorException implements Exception
/**
* Create an ErrorException from an Error.
*
* @deprecated psySH no longer wraps Errors
*
* @param \Error $e
*
* @return ErrorException
* @return self
*/
public static function fromError(\Error $e)
public static function fromError(\Error $e): self
{
return new self($e->getMessage(), $e->getCode(), 1, $e->getFile(), $e->getLine(), $e);
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -21,14 +21,14 @@ class FatalErrorException extends \ErrorException implements Exception
/**
* 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)
* @param string $message (default: "")
* @param int $code (default: 0)
* @param int $severity (default: 1)
* @param string|null $filename (default: null)
* @param int|null $lineno (default: null)
* @param \Throwable|null $previous (default: null)
*/
public function __construct($message = '', $code = 0, $severity = 1, $filename = null, $lineno = null, $previous = null)
public function __construct($message = '', $code = 0, $severity = 1, $filename = null, $lineno = null, \Throwable $previous = null)
{
// Since these are basically always PHP Parser Node line numbers, treat -1 as null.
if ($lineno === -1) {
@@ -36,7 +36,7 @@ class FatalErrorException extends \ErrorException implements Exception
}
$this->rawMessage = $message;
$message = sprintf('PHP Fatal error: %s in %s on line %d', $message, $filename ?: "eval()'d code", $lineno);
$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);
}
@@ -45,7 +45,7 @@ class FatalErrorException extends \ErrorException implements Exception
*
* @return string
*/
public function getRawMessage()
public function getRawMessage(): string
{
return $this->rawMessage;
}

View File

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

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2017 Justin Hileman
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -21,11 +21,11 @@ class RuntimeException extends \RuntimeException implements Exception
/**
* Make this bad boy.
*
* @param string $message (default: "")
* @param int $code (default: 0)
* @param \Exception $previous (default: null)
* @param string $message (default: "")
* @param int $code (default: 0)
* @param \Throwable|null $previous (default: null)
*/
public function __construct($message = '', $code = 0, \Exception $previous = null)
public function __construct(string $message = '', int $code = 0, \Throwable $previous = null)
{
$this->rawMessage = $message;
parent::__construct($message, $code, $previous);
@@ -36,7 +36,7 @@ class RuntimeException extends \RuntimeException implements Exception
*
* @return string
*/
public function getRawMessage()
public function getRawMessage(): string
{
return $this->rawMessage;
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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(\Throwable $throwable)
{
$message = \sprintf("Throwing %s with message '%s'", \get_class($throwable), $throwable->getMessage());
parent::__construct($message, $throwable->getCode(), $throwable);
}
/**
* Return a raw (unformatted) version of the error message.
*
* @return string
*/
public function getRawMessage(): string
{
return $this->getPrevious()->getMessage();
}
/**
* Create a ThrowUpException from a Throwable.
*
* @deprecated psySH no longer wraps Throwables
*
* @param \Throwable $throwable
*
* @return self
*/
public static function fromThrowable($throwable): self
{
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,60 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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!
*
* @deprecated psySH no longer wraps TypeErrors
*
* @param string $message (default: "")
* @param int $code (default: 0)
* @param \Throwable|null $previous (default: null)
*/
public function __construct(string $message = '', int $code = 0, \Throwable $previous = null)
{
$this->rawMessage = $message;
$message = \preg_replace('/, called in .*?: eval\\(\\)\'d code/', '', $message);
parent::__construct(\sprintf('TypeError: %s', $message), $code, $previous);
}
/**
* Get the raw (unformatted) message for this error.
*
* @return string
*/
public function getRawMessage(): string
{
return $this->rawMessage;
}
/**
* Create a TypeErrorException from a TypeError.
*
* @deprecated psySH no longer wraps TypeErrors
*
* @param \TypeError $e
*
* @return self
*/
public static function fromTypeError(\TypeError $e): self
{
return new self($e->getMessage(), $e->getCode(), $e);
}
}

View File

@@ -0,0 +1,37 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Exception;
class UnexpectedTargetException extends RuntimeException
{
private $target;
/**
* @param mixed $target
* @param string $message (default: "")
* @param int $code (default: 0)
* @param \Throwable|null $previous (default: null)
*/
public function __construct($target, string $message = '', int $code = 0, \Throwable $previous = null)
{
$this->target = $target;
parent::__construct($message, $code, $previous);
}
/**
* @return mixed
*/
public function getTarget()
{
return $this->target;
}
}

View File

@@ -0,0 +1,91 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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() ?: self::NOOP_INPUT));
} catch (\Throwable $_e) {
// Clean up on our way out.
if (\ob_get_level() > 0) {
\ob_end_clean();
}
throw $_e;
} finally {
// 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 $shell
* @param \Closure $closure
*/
protected function setClosure(Shell $shell, \Closure $closure)
{
$that = $shell->getBoundObject();
if (\is_object($that)) {
$this->closure = $closure->bindTo($that, \get_class($that));
} else {
$this->closure = $closure->bindTo(null, $shell->getBoundClass());
}
}
/**
* Go go gadget closure.
*
* @return mixed
*/
public function execute()
{
$closure = $this->closure;
return $closure();
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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, string $input)
{
}
/**
* {@inheritdoc}
*/
public function onExecute(Shell $shell, string $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-2022 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(): bool;
/**
* 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, string $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, string $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,289 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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;
private static $pcntlFunctions = [
'pcntl_fork',
'pcntl_signal_dispatch',
'pcntl_signal',
'pcntl_waitpid',
'pcntl_wexitstatus',
];
private static $posixFunctions = [
'posix_getpid',
'posix_kill',
];
/**
* Process forker is supported if pcntl and posix extensions are available.
*
* @return bool
*/
public static function isSupported(): bool
{
return self::isPcntlSupported() && !self::disabledPcntlFunctions() && self::isPosixSupported() && !self::disabledPosixFunctions();
}
/**
* Verify that all required pcntl functions are, in fact, available.
*/
public static function isPcntlSupported(): bool
{
foreach (self::$pcntlFunctions as $func) {
if (!\function_exists($func)) {
return false;
}
}
return true;
}
/**
* Check whether required pcntl functions are disabled.
*/
public static function disabledPcntlFunctions()
{
return self::checkDisabledFunctions(self::$pcntlFunctions);
}
/**
* Verify that all required posix functions are, in fact, available.
*/
public static function isPosixSupported(): bool
{
foreach (self::$posixFunctions as $func) {
if (!\function_exists($func)) {
return false;
}
}
return true;
}
/**
* Check whether required posix functions are disabled.
*/
public static function disabledPosixFunctions()
{
return self::checkDisabledFunctions(self::$posixFunctions);
}
private static function checkDisabledFunctions(array $functions): array
{
return \array_values(\array_intersect($functions, \array_map('strtolower', \array_map('trim', \explode(',', \ini_get('disable_functions'))))));
}
/**
* Forks into a main 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 (!@\cli_set_process_title('psysh (loop)')) {
// Fall back to `setproctitle` if that wasn't succesful.
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): string
{
$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;
}
if (\version_compare(\PHP_VERSION, '8.1', '>=') && $value instanceof \UnitEnum) {
// Enums defined in the REPL session can't be unserialized.
$ref = new \ReflectionObject($value);
if (\strpos($ref->getFileName(), ": eval()'d code") !== false) {
continue;
}
}
try {
@\serialize($value);
$serializable[$key] = $value;
} catch (\Throwable $e) {
// we'll just ignore this one...
}
}
return @\serialize($serializable);
}
}

View File

@@ -0,0 +1,144 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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(): bool
{
// runkit_import was removed in runkit7-4.0.0a1
return \extension_loaded('runkit') || \extension_loaded('runkit7') && \function_exists('runkit_import');
}
/**
* 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, string $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) {
$flags = (
RUNKIT_IMPORT_FUNCTIONS |
RUNKIT_IMPORT_CLASSES |
RUNKIT_IMPORT_CLASS_METHODS |
RUNKIT_IMPORT_CLASS_CONSTS |
RUNKIT_IMPORT_CLASS_PROPS |
RUNKIT_IMPORT_OVERRIDE
);
// these two const cannot be used with RUNKIT_IMPORT_OVERRIDE in runkit7
if (\extension_loaded('runkit7')) {
$flags &= ~RUNKIT_IMPORT_CLASS_PROPS & ~RUNKIT_IMPORT_CLASS_STATIC_PROPS;
runkit7_import($file, $flags);
} else {
runkit_import($file, $flags);
}
}
}
/**
* 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(string $file): bool
{
// first try to parse it
try {
$this->parser->parse(\file_get_contents($file));
} catch (\Throwable $e) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,89 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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\ThrowUpException;
/**
* 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));
while (true) {
$__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.
if (\ob_get_level() > 0) {
\ob_end_clean();
}
throw $_e;
} finally {
// 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 (\Throwable $_e) {
$__psysh__->writeException($_e);
}
$__psysh__->afterLoop();
}
});
}
}

View File

@@ -0,0 +1,320 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2022 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\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* A pretty-printer for code.
*/
class CodeFormatter implements ReflectorFormatter
{
const LINE_MARKER = ' <urgent>></urgent> ';
const NO_LINE_MARKER = ' ';
const HIGHLIGHT_DEFAULT = 'default';
const HIGHLIGHT_KEYWORD = 'keyword';
const HIGHLIGHT_PUBLIC = 'public';
const HIGHLIGHT_PROTECTED = 'protected';
const HIGHLIGHT_PRIVATE = 'private';
const HIGHLIGHT_CONST = 'const';
const HIGHLIGHT_NUMBER = 'number';
const HIGHLIGHT_STRING = 'string';
const HIGHLIGHT_COMMENT = 'comment';
const HIGHLIGHT_INLINE_HTML = 'inline_html';
private static $tokenMap = [
// Not highlighted
\T_OPEN_TAG => self::HIGHLIGHT_DEFAULT,
\T_OPEN_TAG_WITH_ECHO => self::HIGHLIGHT_DEFAULT,
\T_CLOSE_TAG => self::HIGHLIGHT_DEFAULT,
\T_STRING => self::HIGHLIGHT_DEFAULT,
\T_VARIABLE => self::HIGHLIGHT_DEFAULT,
\T_NS_SEPARATOR => self::HIGHLIGHT_DEFAULT,
// Visibility
\T_PUBLIC => self::HIGHLIGHT_PUBLIC,
\T_PROTECTED => self::HIGHLIGHT_PROTECTED,
\T_PRIVATE => self::HIGHLIGHT_PRIVATE,
// Constants
\T_DIR => self::HIGHLIGHT_CONST,
\T_FILE => self::HIGHLIGHT_CONST,
\T_METHOD_C => self::HIGHLIGHT_CONST,
\T_NS_C => self::HIGHLIGHT_CONST,
\T_LINE => self::HIGHLIGHT_CONST,
\T_CLASS_C => self::HIGHLIGHT_CONST,
\T_FUNC_C => self::HIGHLIGHT_CONST,
\T_TRAIT_C => self::HIGHLIGHT_CONST,
// Types
\T_DNUMBER => self::HIGHLIGHT_NUMBER,
\T_LNUMBER => self::HIGHLIGHT_NUMBER,
\T_ENCAPSED_AND_WHITESPACE => self::HIGHLIGHT_STRING,
\T_CONSTANT_ENCAPSED_STRING => self::HIGHLIGHT_STRING,
// Comments
\T_COMMENT => self::HIGHLIGHT_COMMENT,
\T_DOC_COMMENT => self::HIGHLIGHT_COMMENT,
// @todo something better here?
\T_INLINE_HTML => self::HIGHLIGHT_INLINE_HTML,
];
/**
* Format the code represented by $reflector for shell output.
*
* @param \Reflector $reflector
* @param string|null $colorMode (deprecated and ignored)
*
* @return string formatted code
*/
public static function format(\Reflector $reflector, string $colorMode = null): string
{
if (self::isReflectable($reflector)) {
if ($code = @\file_get_contents($reflector->getFileName())) {
return self::formatCode($code, self::getStartLine($reflector), $reflector->getEndLine());
}
}
throw new RuntimeException('Source code unavailable');
}
/**
* Format code for shell output.
*
* Optionally, restrict by $startLine and $endLine line numbers, or pass $markLine to add a line marker.
*
* @param string $code
* @param int $startLine
* @param int|null $endLine
* @param int|null $markLine
*
* @return string formatted code
*/
public static function formatCode(string $code, int $startLine = 1, int $endLine = null, int $markLine = null): string
{
$spans = self::tokenizeSpans($code);
$lines = self::splitLines($spans, $startLine, $endLine);
$lines = self::formatLines($lines);
$lines = self::numberLines($lines, $markLine);
return \implode('', \iterator_to_array($lines));
}
/**
* Get the start line for a given Reflector.
*
* Tries to incorporate doc comments if possible.
*
* This is typehinted as \Reflector but we've narrowed the input via self::isReflectable already.
*
* @param \ReflectionClass|\ReflectionFunctionAbstract $reflector
*
* @return int
*/
private static function getStartLine(\Reflector $reflector): int
{
$startLine = $reflector->getStartLine();
if ($docComment = $reflector->getDocComment()) {
$startLine -= \preg_match_all('/(\r\n?|\n)/', $docComment) + 1;
}
return \max($startLine, 1);
}
/**
* Split code into highlight spans.
*
* Tokenize via \token_get_all, then map these tokens to internal highlight types, combining
* adjacent spans of the same highlight type.
*
* @todo consider switching \token_get_all() out for PHP-Parser-based formatting at some point.
*
* @param string $code
*
* @return \Generator [$spanType, $spanText] highlight spans
*/
private static function tokenizeSpans(string $code): \Generator
{
$spanType = null;
$buffer = '';
foreach (\token_get_all($code) as $token) {
$nextType = self::nextHighlightType($token, $spanType);
$spanType = $spanType ?: $nextType;
if ($spanType !== $nextType) {
yield [$spanType, $buffer];
$spanType = $nextType;
$buffer = '';
}
$buffer .= \is_array($token) ? $token[1] : $token;
}
if ($spanType !== null && $buffer !== '') {
yield [$spanType, $buffer];
}
}
/**
* Given a token and the current highlight span type, compute the next type.
*
* @param array|string $token \token_get_all token
* @param string|null $currentType
*
* @return string|null
*/
private static function nextHighlightType($token, $currentType)
{
if ($token === '"') {
return self::HIGHLIGHT_STRING;
}
if (\is_array($token)) {
if ($token[0] === \T_WHITESPACE) {
return $currentType;
}
if (\array_key_exists($token[0], self::$tokenMap)) {
return self::$tokenMap[$token[0]];
}
}
return self::HIGHLIGHT_KEYWORD;
}
/**
* Group highlight spans into an array of lines.
*
* Optionally, restrict by start and end line numbers.
*
* @param \Generator $spans as [$spanType, $spanText] pairs
* @param int $startLine
* @param int|null $endLine
*
* @return \Generator lines, each an array of [$spanType, $spanText] pairs
*/
private static function splitLines(\Generator $spans, int $startLine = 1, int $endLine = null): \Generator
{
$lineNum = 1;
$buffer = [];
foreach ($spans as list($spanType, $spanText)) {
foreach (\preg_split('/(\r\n?|\n)/', $spanText) as $index => $spanLine) {
if ($index > 0) {
if ($lineNum >= $startLine) {
yield $lineNum => $buffer;
}
$lineNum++;
$buffer = [];
if ($endLine !== null && $lineNum > $endLine) {
return;
}
}
if ($spanLine !== '') {
$buffer[] = [$spanType, $spanLine];
}
}
}
if (!empty($buffer)) {
yield $lineNum => $buffer;
}
}
/**
* Format lines of highlight spans for shell output.
*
* @param \Generator $spanLines lines, each an array of [$spanType, $spanText] pairs
*
* @return \Generator Formatted lines
*/
private static function formatLines(\Generator $spanLines): \Generator
{
foreach ($spanLines as $lineNum => $spanLine) {
$line = '';
foreach ($spanLine as list($spanType, $spanText)) {
if ($spanType === self::HIGHLIGHT_DEFAULT) {
$line .= OutputFormatter::escape($spanText);
} else {
$line .= \sprintf('<%s>%s</%s>', $spanType, OutputFormatter::escape($spanText), $spanType);
}
}
yield $lineNum => $line.\PHP_EOL;
}
}
/**
* Prepend line numbers to formatted lines.
*
* Lines must be in an associative array with the correct keys in order to be numbered properly.
*
* Optionally, pass $markLine to add a line marker.
*
* @param \Generator $lines Formatted lines
* @param int|null $markLine
*
* @return \Generator Numbered, formatted lines
*/
private static function numberLines(\Generator $lines, int $markLine = null): \Generator
{
$lines = \iterator_to_array($lines);
// Figure out how much space to reserve for line numbers.
\end($lines);
$pad = \strlen(\key($lines));
// If $markLine is before or after our line range, don't bother reserving space for the marker.
if ($markLine !== null) {
if ($markLine > \key($lines)) {
$markLine = null;
}
\reset($lines);
if ($markLine < \key($lines)) {
$markLine = null;
}
}
foreach ($lines as $lineNum => $line) {
$mark = '';
if ($markLine !== null) {
$mark = ($markLine === $lineNum) ? self::LINE_MARKER : self::NO_LINE_MARKER;
}
yield \sprintf("%s<aside>%{$pad}s</aside>: %s", $mark, $lineNum, $line);
}
}
/**
* Check whether a Reflector instance is reflectable by this formatter.
*
* @param \Reflector $reflector
*
* @return bool
*/
private static function isReflectable(\Reflector $reflector): bool
{
return ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionFunctionAbstract) && \is_file($reflector->getFileName());
}
}

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