refactor: susun semula struktur folder — Laravel source ke src/
This commit is contained in:
21
vendor/psy/psysh/LICENSE
vendored
Normal file
21
vendor/psy/psysh/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012-2026 Justin Hileman
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||
OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
36
vendor/psy/psysh/README.md
vendored
Normal file
36
vendor/psy/psysh/README.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# PsySH
|
||||
|
||||
PsySH is a runtime developer console, interactive debugger and [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) for PHP. Learn more at [psysh.org](http://psysh.org/) and [in the manual](https://github.com/bobthecow/psysh/wiki/Home).
|
||||
|
||||
|
||||
[](https://packagist.org/packages/psy/psysh)
|
||||
[](https://packagist.org/packages/psy/psysh)
|
||||
[](http://psysh.org)
|
||||
|
||||
[](https://github.com/bobthecow/psysh/actions?query=branch:main)
|
||||
[](https://styleci.io/repos/4549925)
|
||||
|
||||
|
||||
<a id="downloading-the-manual"></a>
|
||||
|
||||
## [PsySH manual](https://github.com/bobthecow/psysh/wiki/Home)
|
||||
|
||||
### [💾 Installation](https://github.com/bobthecow/psysh/wiki/Installation)
|
||||
* [📕 PHP manual](https://github.com/bobthecow/psysh/wiki/PHP-manual)
|
||||
* [🤓 Windows](https://github.com/bobthecow/psysh/wiki/Windows)
|
||||
|
||||
### [🖥 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)
|
||||
375
vendor/psy/psysh/bin/psysh
vendored
Normal file
375
vendor/psy/psysh/bin/psysh
vendored
Normal file
@@ -0,0 +1,375 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
// Try to find an autoloader for a local psysh version.
|
||||
// We'll wrap this whole mess in a Closure so it doesn't leak any globals.
|
||||
call_user_func(function () {
|
||||
$cwd = null;
|
||||
$cwdFromArg = false;
|
||||
$forceTrust = false;
|
||||
$forceUntrust = false;
|
||||
|
||||
// Find the cwd arg (if present)
|
||||
$argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : array();
|
||||
foreach ($argv as $i => $arg) {
|
||||
if ($arg === '--cwd') {
|
||||
if ($i >= count($argv) - 1) {
|
||||
fwrite(STDERR, 'Missing --cwd argument.' . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
$cwd = $argv[$i + 1];
|
||||
$cwdFromArg = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^--cwd=/', $arg)) {
|
||||
$cwd = substr($arg, 6);
|
||||
$cwdFromArg = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($arg === '--trust-project') {
|
||||
$forceTrust = true;
|
||||
$forceUntrust = false;
|
||||
} elseif ($arg === '--no-trust-project') {
|
||||
$forceUntrust = true;
|
||||
$forceTrust = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($cwdFromArg) {
|
||||
if (!@chdir($cwd)) {
|
||||
fwrite(STDERR, 'Invalid --cwd directory: ' . $cwd . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the actual cwd, or normalize the path after chdir
|
||||
if (!isset($cwd) || $cwdFromArg) {
|
||||
$cwd = getcwd();
|
||||
}
|
||||
|
||||
$cwd = str_replace('\\', '/', $cwd);
|
||||
|
||||
if ($cwdFromArg) {
|
||||
$filtered = array();
|
||||
$skipNext = false;
|
||||
foreach ($argv as $arg) {
|
||||
if ($skipNext) {
|
||||
$skipNext = false;
|
||||
continue;
|
||||
}
|
||||
if ($arg === '--cwd') {
|
||||
$skipNext = true;
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^--cwd=/', $arg)) {
|
||||
continue;
|
||||
}
|
||||
$filtered[] = $arg;
|
||||
}
|
||||
$_SERVER['argv'] = $filtered;
|
||||
$_SERVER['argc'] = count($filtered);
|
||||
$argv = $filtered;
|
||||
}
|
||||
|
||||
if (isset($_SERVER['PSYSH_TRUST_PROJECT']) && $_SERVER['PSYSH_TRUST_PROJECT'] !== '') {
|
||||
$mode = strtolower(trim($_SERVER['PSYSH_TRUST_PROJECT']));
|
||||
if (in_array($mode, array('true', '1'))) {
|
||||
$forceTrust = true;
|
||||
$forceUntrust = false;
|
||||
} elseif (in_array($mode, array('false', '0'))) {
|
||||
$forceUntrust = true;
|
||||
$forceTrust = false;
|
||||
} else {
|
||||
fwrite(STDERR, 'Invalid PSYSH_TRUST_PROJECT value: ' . $_SERVER['PSYSH_TRUST_PROJECT'] . '. Expected: true, 1, false, or 0.' . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass trust decision via env var and strip CLI flags. This allows a local
|
||||
// psysh version to read the trust state while avoiding errors on older
|
||||
// versions that don't understand --trust-project flags.
|
||||
if ($forceTrust) {
|
||||
$_SERVER['PSYSH_TRUST_PROJECT'] = 'true';
|
||||
$_ENV['PSYSH_TRUST_PROJECT'] = 'true';
|
||||
putenv('PSYSH_TRUST_PROJECT=true');
|
||||
} elseif ($forceUntrust) {
|
||||
$_SERVER['PSYSH_TRUST_PROJECT'] = 'false';
|
||||
$_ENV['PSYSH_TRUST_PROJECT'] = 'false';
|
||||
putenv('PSYSH_TRUST_PROJECT=false');
|
||||
}
|
||||
|
||||
if ($forceTrust || $forceUntrust) {
|
||||
$filtered = array();
|
||||
foreach ($argv as $arg) {
|
||||
if ($arg === '--trust-project' || $arg === '--no-trust-project') {
|
||||
continue;
|
||||
}
|
||||
$filtered[] = $arg;
|
||||
}
|
||||
$_SERVER['argv'] = $filtered;
|
||||
$_SERVER['argc'] = count($filtered);
|
||||
$argv = $filtered;
|
||||
}
|
||||
|
||||
$trustedRoots = array();
|
||||
if (!$forceTrust) {
|
||||
// Find the current config directory (matching ConfigPaths logic)
|
||||
$currentConfigDir = null;
|
||||
$fallbackConfigDir = null;
|
||||
|
||||
// Windows: %APPDATA%/PsySH takes priority
|
||||
if ($currentConfigDir === null && defined('PHP_WINDOWS_VERSION_MAJOR')) {
|
||||
if (isset($_SERVER['APPDATA']) && $_SERVER['APPDATA']) {
|
||||
$dir = str_replace('\\', '/', $_SERVER['APPDATA']).'/PsySH';
|
||||
$fallbackConfigDir = $fallbackConfigDir !== null ? $fallbackConfigDir : $dir;
|
||||
if (@is_dir($dir)) {
|
||||
$currentConfigDir = $dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// XDG_CONFIG_HOME/psysh
|
||||
if ($currentConfigDir === null && isset($_SERVER['XDG_CONFIG_HOME']) && $_SERVER['XDG_CONFIG_HOME']) {
|
||||
$dir = rtrim(str_replace('\\', '/', $_SERVER['XDG_CONFIG_HOME']), '/').'/psysh';
|
||||
$fallbackConfigDir = $fallbackConfigDir !== null ? $fallbackConfigDir : $dir;
|
||||
if (@is_dir($dir)) {
|
||||
$currentConfigDir = $dir;
|
||||
}
|
||||
}
|
||||
|
||||
// HOME/.config/psysh (default XDG location)
|
||||
if ($currentConfigDir === null && isset($_SERVER['HOME']) && $_SERVER['HOME']) {
|
||||
$home = rtrim(str_replace('\\', '/', $_SERVER['HOME']), '/');
|
||||
|
||||
$dir = $home.'/.config/psysh';
|
||||
$fallbackConfigDir = $fallbackConfigDir !== null ? $fallbackConfigDir : $dir;
|
||||
if (@is_dir($dir)) {
|
||||
$currentConfigDir = $dir;
|
||||
}
|
||||
|
||||
// legacy
|
||||
if ($currentConfigDir === null) {
|
||||
$dir = $home.'/.psysh';
|
||||
if (@is_dir($dir)) {
|
||||
$currentConfigDir = $dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Windows: HOMEDRIVE/HOMEPATH fallback
|
||||
if ($currentConfigDir === null && defined('PHP_WINDOWS_VERSION_MAJOR')) {
|
||||
if (isset($_SERVER['HOMEDRIVE']) && isset($_SERVER['HOMEPATH']) && $_SERVER['HOMEDRIVE'] && $_SERVER['HOMEPATH']) {
|
||||
$dir = rtrim(str_replace('\\', '/', $_SERVER['HOMEDRIVE'].'/'.$_SERVER['HOMEPATH']), '/').'/.psysh';
|
||||
if (@is_dir($dir)) {
|
||||
$currentConfigDir = $dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the first candidate directory if none exist yet
|
||||
if ($currentConfigDir === null) {
|
||||
$currentConfigDir = $fallbackConfigDir;
|
||||
}
|
||||
|
||||
if ($currentConfigDir !== null) {
|
||||
$trustFile = $currentConfigDir.'/trusted_projects.json';
|
||||
if (is_file($trustFile)) {
|
||||
$contents = file_get_contents($trustFile);
|
||||
if ($contents !== false && $contents !== '') {
|
||||
$data = json_decode($contents, true);
|
||||
if (is_array($data)) {
|
||||
foreach ($data as $dir) {
|
||||
if (!is_string($dir) || $dir === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$real = realpath($dir);
|
||||
if ($real !== false) {
|
||||
$dir = $real;
|
||||
}
|
||||
|
||||
$trustedRoots[] = str_replace('\\', '/', $dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Composer-generated bin proxies expose `_composer_autoload_path`, which points
|
||||
// at the autoloader for the *current* project invoking `vendor/bin/psysh`.
|
||||
// We use this to distinguish "already running via this project's local psysh"
|
||||
// from "global psysh trying to hop into some other project's local psysh".
|
||||
$proxyAutoloadPath = null;
|
||||
if (isset($GLOBALS['_composer_autoload_path'])
|
||||
&& is_string($GLOBALS['_composer_autoload_path'])
|
||||
&& $GLOBALS['_composer_autoload_path'] !== ''
|
||||
) {
|
||||
$proxyAutoloadPath = realpath($GLOBALS['_composer_autoload_path']);
|
||||
if ($proxyAutoloadPath === false) {
|
||||
$proxyAutoloadPath = str_replace('\\', '/', $GLOBALS['_composer_autoload_path']);
|
||||
} else {
|
||||
$proxyAutoloadPath = str_replace('\\', '/', $proxyAutoloadPath);
|
||||
}
|
||||
}
|
||||
|
||||
$isCurrentProjectAutoload = function ($projectPath) use ($proxyAutoloadPath) {
|
||||
if ($proxyAutoloadPath === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$projectAutoloadPath = realpath($projectPath.'/vendor/autoload.php');
|
||||
if ($projectAutoloadPath === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str_replace('\\', '/', $projectAutoloadPath) === $proxyAutoloadPath;
|
||||
};
|
||||
|
||||
$markUntrustedProject = function ($projectPath, $prettyPath) {
|
||||
fwrite(STDERR, 'Skipping local PsySH at ' . $prettyPath . ' (project is untrusted). Re-run with --trust-project to allow.' . PHP_EOL);
|
||||
$_SERVER['PSYSH_UNTRUSTED_PROJECT'] = $projectPath;
|
||||
$_ENV['PSYSH_UNTRUSTED_PROJECT'] = $projectPath;
|
||||
putenv('PSYSH_UNTRUSTED_PROJECT='.$projectPath);
|
||||
};
|
||||
|
||||
$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.
|
||||
if (is_file($path . '/vendor/autoload.php')) {
|
||||
$realPath = realpath($path);
|
||||
$realPath = $realPath ? str_replace('\\', '/', $realPath) : $path;
|
||||
$pathReal = realpath($path);
|
||||
$binReal = realpath(__DIR__ . '/..');
|
||||
$isCurrentPsysh = ($pathReal !== false && $pathReal === $binReal) || $isCurrentProjectAutoload($path);
|
||||
|
||||
if (!$isCurrentPsysh && !$forceTrust && ($forceUntrust || !in_array($realPath, $trustedRoots, true))) {
|
||||
$markUntrustedProject($realPath, $prettyPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$isCurrentPsysh) {
|
||||
fwrite(STDERR, 'Using local PsySH version at ' . $prettyPath . PHP_EOL);
|
||||
}
|
||||
|
||||
require $path . '/vendor/autoload.php';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Or a composer.lock
|
||||
if (is_file($path . '/composer.lock')) {
|
||||
if ($cfg = json_decode(file_get_contents($path . '/composer.lock'), true)) {
|
||||
$packages = array_merge(isset($cfg['packages']) ? $cfg['packages'] : array(), isset($cfg['packages-dev']) ? $cfg['packages-dev'] : array());
|
||||
foreach ($packages as $pkg) {
|
||||
if (isset($pkg['name']) && $pkg['name'] === 'psy/psysh') {
|
||||
// We're inside a project which requires psysh. We'll use the local Composer autoload.
|
||||
if (is_file($path . '/vendor/autoload.php')) {
|
||||
$realPath = realpath($path);
|
||||
$realPath = $realPath ? str_replace('\\', '/', $realPath) : $path;
|
||||
$vendorReal = realpath($path . '/vendor');
|
||||
$binVendorReal = realpath(__DIR__ . '/../../..');
|
||||
$isCurrentPsysh = ($vendorReal !== false && $vendorReal === $binVendorReal) || $isCurrentProjectAutoload($path);
|
||||
|
||||
if (!$isCurrentPsysh && !$forceTrust && ($forceUntrust || !in_array($realPath, $trustedRoots, true))) {
|
||||
$markUntrustedProject($realPath, $prettyPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$isCurrentPsysh) {
|
||||
fwrite(STDERR, 'Using local PsySH version at ' . $prettyPath . PHP_EOL);
|
||||
}
|
||||
|
||||
require $path . '/vendor/autoload.php';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
array_pop($chunks);
|
||||
}
|
||||
});
|
||||
|
||||
// We didn't find an autoloader for a local version, so use the autoloader that
|
||||
// came with this script.
|
||||
if (!class_exists('Psy\Shell')) {
|
||||
/* <<< */
|
||||
if (is_file(__DIR__ . '/../vendor/autoload.php')) {
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
} elseif (is_file(__DIR__ . '/../../../autoload.php')) {
|
||||
require __DIR__ . '/../../../autoload.php';
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
/* >>> */
|
||||
}
|
||||
|
||||
// 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 (PHP_VERSION_ID < 50306) {
|
||||
$trace = debug_backtrace();
|
||||
} elseif (PHP_VERSION_ID < 50400) {
|
||||
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
} else {
|
||||
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
|
||||
}
|
||||
|
||||
if (Psy\Shell::isIncluded($trace)) {
|
||||
unset($trace);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up after ourselves.
|
||||
unset($trace);
|
||||
|
||||
// If the local version is too old, we can't do this
|
||||
if (!function_exists('Psy\bin')) {
|
||||
$argv = 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');
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// And go!
|
||||
call_user_func(Psy\bin());
|
||||
60
vendor/psy/psysh/composer.json
vendored
Normal file
60
vendor/psy/psysh/composer.json
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "psy/psysh",
|
||||
"description": "An interactive shell for modern PHP.",
|
||||
"type": "library",
|
||||
"keywords": ["console", "interactive", "shell", "repl"],
|
||||
"homepage": "https://psysh.org",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Justin Hileman",
|
||||
"email": "justin@justinhileman.info"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.0 || ^7.4",
|
||||
"ext-json": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"nikic/php-parser": "^5.0 || ^4.0",
|
||||
"symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4",
|
||||
"symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.2",
|
||||
"composer/class-map-generator": "^1.6"
|
||||
},
|
||||
"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.",
|
||||
"composer/class-map-generator": "Improved tab completion performance with better class discovery."
|
||||
},
|
||||
"autoload": {
|
||||
"files": ["src/functions.php"],
|
||||
"psr-4": {
|
||||
"Psy\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Psy\\Test\\": "test/"
|
||||
}
|
||||
},
|
||||
"bin": ["bin/psysh"],
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"bamarni/composer-bin-plugin": true
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "0.12.x-dev"
|
||||
},
|
||||
"bamarni-bin": {
|
||||
"bin-links": false,
|
||||
"forward-command": false
|
||||
}
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4"
|
||||
}
|
||||
}
|
||||
19
vendor/psy/psysh/src/Clipboard/ClipboardMethod.php
vendored
Normal file
19
vendor/psy/psysh/src/Clipboard/ClipboardMethod.php
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Clipboard;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
interface ClipboardMethod
|
||||
{
|
||||
public function copy(string $text, OutputInterface $output): bool;
|
||||
}
|
||||
68
vendor/psy/psysh/src/Clipboard/CommandClipboardMethod.php
vendored
Normal file
68
vendor/psy/psysh/src/Clipboard/CommandClipboardMethod.php
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Clipboard;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class CommandClipboardMethod implements ClipboardMethod
|
||||
{
|
||||
private string $command;
|
||||
|
||||
public function __construct(string $command)
|
||||
{
|
||||
$this->command = $command;
|
||||
}
|
||||
|
||||
public function copy(string $text, OutputInterface $output): bool
|
||||
{
|
||||
$process = \proc_open($this->command, [
|
||||
0 => ['pipe', 'r'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
], $pipes);
|
||||
if ($process === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$success = $this->writeAll($pipes[0], $text);
|
||||
\fclose($pipes[0]);
|
||||
|
||||
// Drain stdout and stderr to prevent the child process from blocking.
|
||||
\stream_get_contents($pipes[1]);
|
||||
\fclose($pipes[1]);
|
||||
\stream_get_contents($pipes[2]);
|
||||
\fclose($pipes[2]);
|
||||
|
||||
return $success && \proc_close($process) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the full string to the pipe, returning false on failure.
|
||||
*
|
||||
* @param resource $pipe
|
||||
*/
|
||||
private function writeAll($pipe, string $text): bool
|
||||
{
|
||||
$remaining = $text;
|
||||
|
||||
while ($remaining !== '') {
|
||||
$written = \fwrite($pipe, $remaining);
|
||||
if ($written === false || $written === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$remaining = (string) \substr($remaining, $written);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
57
vendor/psy/psysh/src/Clipboard/NullClipboardMethod.php
vendored
Normal file
57
vendor/psy/psysh/src/Clipboard/NullClipboardMethod.php
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Clipboard;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class NullClipboardMethod implements ClipboardMethod
|
||||
{
|
||||
public const REASON_NONE = 'none';
|
||||
public const REASON_NO_COMMAND = 'no_command';
|
||||
public const REASON_NO_COMMAND_SUPPORT = 'no_command_support';
|
||||
|
||||
private bool $warned = false;
|
||||
private bool $isSsh;
|
||||
private string $reason;
|
||||
|
||||
public function __construct(bool $isSsh, string $reason = self::REASON_NONE)
|
||||
{
|
||||
$this->isSsh = $isSsh;
|
||||
$this->reason = $reason;
|
||||
}
|
||||
|
||||
public function copy(string $text, OutputInterface $output): bool
|
||||
{
|
||||
if ($this->warned) {
|
||||
return false;
|
||||
}
|
||||
$this->warned = true;
|
||||
|
||||
if ($this->isSsh) {
|
||||
$output->writeln('<error>Clipboard copy is unavailable over SSH.</error>');
|
||||
$output->writeln('Set <comment>useOsc52Clipboard: true</comment> to enable OSC 52.');
|
||||
$output->writeln('Only enable this on trusted systems.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->reason === self::REASON_NO_COMMAND_SUPPORT) {
|
||||
$output->writeln('<error>Clipboard commands are unavailable in this PHP environment.</error>');
|
||||
$output->writeln('Configured <comment>clipboardCommand</comment> requires <comment>proc_open</comment>.');
|
||||
} elseif ($this->reason === self::REASON_NO_COMMAND) {
|
||||
$output->writeln('<error>No clipboard command was found.</error>');
|
||||
$output->writeln('Set <comment>clipboardCommand</comment> to configure one.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
31
vendor/psy/psysh/src/Clipboard/Osc52ClipboardMethod.php
vendored
Normal file
31
vendor/psy/psysh/src/Clipboard/Osc52ClipboardMethod.php
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Clipboard;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Osc52ClipboardMethod implements ClipboardMethod
|
||||
{
|
||||
public function copy(string $text, OutputInterface $output): bool
|
||||
{
|
||||
$base64 = \base64_encode($text);
|
||||
$osc52 = "\x1b]52;c;{$base64}\x07";
|
||||
|
||||
if (\getenv('TMUX')) {
|
||||
$osc52 = "\x1bPtmux;\x1b".\str_replace("\x1b", "\x1b\x1b", $osc52)."\x1b\\";
|
||||
}
|
||||
|
||||
$output->write($osc52, false, OutputInterface::OUTPUT_RAW);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
188
vendor/psy/psysh/src/CodeAnalysis/BufferAnalysis.php
vendored
Normal file
188
vendor/psy/psysh/src/CodeAnalysis/BufferAnalysis.php
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeAnalysis;
|
||||
|
||||
use PhpParser\Error;
|
||||
use Psy\Readline\Interactive\Helper\TokenHelper;
|
||||
|
||||
/**
|
||||
* Cached analysis data for a code buffer snapshot.
|
||||
*/
|
||||
class BufferAnalysis
|
||||
{
|
||||
private string $code;
|
||||
|
||||
/** @var array<int, array|string> */
|
||||
private array $tokens;
|
||||
|
||||
/** @var array<int, array{start: int, end: int}> */
|
||||
private array $tokenPositions;
|
||||
|
||||
/** @var array<int, mixed>|null */
|
||||
private ?array $ast;
|
||||
private ?Error $lastError;
|
||||
|
||||
/**
|
||||
* @param array<int, array|string> $tokens
|
||||
* @param array<int, array{start: int, end: int}> $tokenPositions
|
||||
* @param array<int, mixed>|null $ast
|
||||
*/
|
||||
public function __construct(string $code, array $tokens, array $tokenPositions, ?array $ast, ?Error $lastError)
|
||||
{
|
||||
$this->code = $code;
|
||||
$this->tokens = $tokens;
|
||||
$this->tokenPositions = $tokenPositions;
|
||||
$this->ast = $ast;
|
||||
$this->lastError = $lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the token_get_all() tokens for the buffer.
|
||||
*
|
||||
* @return array<int, array|string>
|
||||
*/
|
||||
public function getTokens(): array
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get start/end code-point positions for each token.
|
||||
*
|
||||
* @return array<int, array{start: int, end: int}>
|
||||
*/
|
||||
public function getTokenPositions(): array
|
||||
{
|
||||
return $this->tokenPositions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parsed AST, or null if parsing failed.
|
||||
*
|
||||
* @return array<int, mixed>|null
|
||||
*/
|
||||
public function getAst(): ?array
|
||||
{
|
||||
return $this->ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last parse error, if any.
|
||||
*/
|
||||
public function getLastError(): ?Error
|
||||
{
|
||||
return $this->lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the buffer has balanced (), [] and {} pairs.
|
||||
*/
|
||||
public function hasBalancedBrackets(): bool
|
||||
{
|
||||
$stack = [];
|
||||
$pairs = ['(' => ')', '[' => ']', '{' => '}'];
|
||||
|
||||
foreach ($this->tokens as $token) {
|
||||
if (\is_array($token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($pairs[$token])) {
|
||||
$stack[] = $token;
|
||||
} elseif (\in_array($token, $pairs, true)) {
|
||||
if (empty($stack)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$last = \array_pop($stack);
|
||||
if ($pairs[$last] !== $token) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return empty($stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the buffer ends with an operator that requires more input.
|
||||
*/
|
||||
public function hasTrailingOperator(): bool
|
||||
{
|
||||
return TokenHelper::hasTrailingOperator($this->tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the token stream ends inside a string or comment context.
|
||||
*/
|
||||
public function endsInOpenStringOrComment(): bool
|
||||
{
|
||||
if ($this->tokens === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$last = $this->tokens[\count($this->tokens) - 1];
|
||||
|
||||
return $last === '"' || $last === '`' ||
|
||||
(\is_array($last) && \in_array($last[0], [\T_ENCAPSED_AND_WHITESPACE, \T_START_HEREDOC, \T_COMMENT], true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the buffer ends with a control structure header that still needs a body.
|
||||
*/
|
||||
public function hasControlStructureWithoutBody(): bool
|
||||
{
|
||||
$trimmed = \rtrim($this->code);
|
||||
|
||||
if (\preg_match('/\b(if|while|for|foreach|elseif)\s*\(.*\)\s*$/', $trimmed)) {
|
||||
$lastParen = \strrpos($trimmed, ')');
|
||||
if ($lastParen !== false) {
|
||||
$afterParen = \trim(\substr($trimmed, $lastParen + 1));
|
||||
if ($afterParen === '') {
|
||||
if ($this->lastError !== null && !$this->isEOFError($this->lastError)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$isElseAfterBrace = \preg_match('/\}\s*else\s*$/', $trimmed);
|
||||
$isBareElse = \preg_match('/^\s*else\s*$/', $trimmed);
|
||||
|
||||
if ($isElseAfterBrace || $isBareElse) {
|
||||
if ($isBareElse && $this->lastError !== null && !$this->isEOFError($this->lastError)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the last parse error is an unexpected-EOF error.
|
||||
*/
|
||||
public function hasEOFError(): bool
|
||||
{
|
||||
return $this->lastError !== null && $this->isEOFError($this->lastError);
|
||||
}
|
||||
|
||||
private function isEOFError(Error $error): bool
|
||||
{
|
||||
$msg = $error->getRawMessage();
|
||||
|
||||
return ($msg === 'Unexpected token EOF') || (\strpos($msg, 'Syntax error, unexpected EOF') !== false);
|
||||
}
|
||||
}
|
||||
117
vendor/psy/psysh/src/CodeAnalysis/BufferAnalyzer.php
vendored
Normal file
117
vendor/psy/psysh/src/CodeAnalysis/BufferAnalyzer.php
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeAnalysis;
|
||||
|
||||
use PhpParser\Error;
|
||||
use PhpParser\Parser;
|
||||
use Psy\ParserFactory;
|
||||
|
||||
/**
|
||||
* Maintains a cached analysis snapshot for code buffers.
|
||||
*
|
||||
* Uses a small LRU cache (2 entries) to avoid thrashing when callers
|
||||
* alternate between full-buffer and partial (before-cursor) text.
|
||||
*/
|
||||
class BufferAnalyzer
|
||||
{
|
||||
private const CACHE_SIZE = 2;
|
||||
|
||||
private Parser $parser;
|
||||
|
||||
/**
|
||||
* LRU cache of recent analyses, most-recently-used first.
|
||||
*
|
||||
* @var array<int, array{code: string, analysis: BufferAnalysis}>
|
||||
*/
|
||||
private array $cache = [];
|
||||
|
||||
public function __construct(?Parser $parser = null)
|
||||
{
|
||||
$this->parser = $parser ?? (new ParserFactory())->createParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze the given code, using cached data when possible.
|
||||
*/
|
||||
public function analyze(string $code): BufferAnalysis
|
||||
{
|
||||
foreach ($this->cache as $i => $entry) {
|
||||
if ($entry['code'] === $code) {
|
||||
if ($i > 0) {
|
||||
\array_splice($this->cache, $i, 1);
|
||||
\array_unshift($this->cache, $entry);
|
||||
}
|
||||
|
||||
return $entry['analysis'];
|
||||
}
|
||||
}
|
||||
|
||||
$analysis = $this->buildAnalysis($code);
|
||||
|
||||
\array_unshift($this->cache, ['code' => $code, 'analysis' => $analysis]);
|
||||
if (\count($this->cache) > self::CACHE_SIZE) {
|
||||
\array_pop($this->cache);
|
||||
}
|
||||
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether appending a semicolon would make the code parseable.
|
||||
*/
|
||||
public function canBeFixedWithSemicolon(string $code): bool
|
||||
{
|
||||
try {
|
||||
$this->parser->parse('<?php '.$code.";\n");
|
||||
|
||||
return true;
|
||||
} catch (Error $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function buildAnalysis(string $code): BufferAnalysis
|
||||
{
|
||||
$tokens = @\token_get_all('<?php '.$code);
|
||||
|
||||
$tokenPositions = [];
|
||||
$position = 0;
|
||||
foreach ($tokens as $index => $token) {
|
||||
$text = \is_array($token) ? $token[1] : $token;
|
||||
$length = \mb_strlen($text);
|
||||
$tokenPositions[$index] = ['start' => $position, 'end' => $position + $length];
|
||||
$position += $length;
|
||||
}
|
||||
|
||||
$ast = null;
|
||||
$lastError = null;
|
||||
try {
|
||||
// Trailing newline improves heredoc EOF behavior to match runtime checks.
|
||||
$ast = $this->parser->parse('<?php '.$code."\n");
|
||||
} catch (Error $e) {
|
||||
$lastError = $e;
|
||||
}
|
||||
|
||||
return $this->createAnalysis($code, $tokens, $tokenPositions, $ast, $lastError);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
* @param array<int, array|string> $tokens
|
||||
* @param array<int, array{start: int, end: int}> $tokenPositions
|
||||
* @param array<int, mixed>|null $ast
|
||||
*/
|
||||
protected function createAnalysis(string $code, array $tokens, array $tokenPositions, ?array $ast, ?Error $lastError): BufferAnalysis
|
||||
{
|
||||
return new BufferAnalysis($code, $tokens, $tokenPositions, $ast, $lastError);
|
||||
}
|
||||
}
|
||||
711
vendor/psy/psysh/src/CodeCleaner.php
vendored
Normal file
711
vendor/psy/psysh/src/CodeCleaner.php
vendored
Normal file
@@ -0,0 +1,711 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\Error as PhpParserError;
|
||||
use PhpParser\Node\Expr\ClassConstFetch;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\Name\FullyQualified;
|
||||
use PhpParser\Node\Stmt\Expression;
|
||||
use PhpParser\Node\Stmt\Namespace_;
|
||||
use PhpParser\Node\Stmt\Use_;
|
||||
use PhpParser\NodeTraverser;
|
||||
use PhpParser\NodeVisitor\NameResolver;
|
||||
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\CodeCleanerPass;
|
||||
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\ImplicitUsePass;
|
||||
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\NamespaceAwarePass;
|
||||
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;
|
||||
use Psy\Util\Str;
|
||||
|
||||
/**
|
||||
* 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 bool $yolo = false;
|
||||
private bool $strictTypes = false;
|
||||
private $implicitUse = false;
|
||||
|
||||
private Parser $parser;
|
||||
private Printer $printer;
|
||||
private NodeTraverser $traverser;
|
||||
private ?array $namespace = null;
|
||||
private array $messages = [];
|
||||
private array $aliasesByNamespace = [];
|
||||
private array $aliasesByTypeByNamespace = [];
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param bool $strictTypes enforce strict types by default
|
||||
* @param false|array $implicitUse disable implicit use statements (false) or configure with namespace filters (array)
|
||||
*/
|
||||
public function __construct(?Parser $parser = null, ?Printer $printer = null, ?NodeTraverser $traverser = null, bool $yolo = false, bool $strictTypes = false, $implicitUse = false)
|
||||
{
|
||||
$this->yolo = $yolo;
|
||||
$this->strictTypes = $strictTypes;
|
||||
$this->implicitUse = \is_array($implicitUse) ? $implicitUse : false;
|
||||
|
||||
$this->parser = $parser ?? (new ParserFactory())->createParser();
|
||||
$this->printer = $printer ?: new Printer();
|
||||
$this->traverser = $traverser ?: new NodeTraverser();
|
||||
|
||||
// Try to add implicit `use` statements and an implicit namespace, based on the file in
|
||||
// which the `debug` call was made.
|
||||
$this->addImplicitDebugContext();
|
||||
|
||||
foreach ($this->getDefaultPasses() as $pass) {
|
||||
$this->traverser->addVisitor($pass);
|
||||
|
||||
// Set CodeCleaner instance on NamespaceAwarePass for state management
|
||||
if ($pass instanceof NamespaceAwarePass) {
|
||||
$pass->setCleaner($this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this CodeCleaner is in YOLO mode.
|
||||
*/
|
||||
public function yolo(): bool
|
||||
{
|
||||
return $this->yolo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default CodeCleaner passes.
|
||||
*
|
||||
* @return CodeCleanerPass[]
|
||||
*/
|
||||
private function getDefaultPasses(): array
|
||||
{
|
||||
// Add implicit use pass if enabled (must run before use statement pass)
|
||||
$usePasses = [new UseStatementPass()];
|
||||
if ($this->implicitUse) {
|
||||
\array_unshift($usePasses, new ImplicitUsePass($this->implicitUse, $this));
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// When in --yolo mode, these are the only code cleaner passes used.
|
||||
$rewritePasses = [
|
||||
new LeavePsyshAlonePass(),
|
||||
new ExitPass(),
|
||||
new ImplicitReturnPass(),
|
||||
new MagicConstantsPass(),
|
||||
new NamespacePass(), // must run after the implicit return pass
|
||||
...$usePasses, // must run after the namespace pass has re-injected the current namespace
|
||||
new RequirePass(),
|
||||
new StrictTypesPass($this->strictTypes),
|
||||
];
|
||||
|
||||
if ($this->yolo) {
|
||||
return $rewritePasses;
|
||||
}
|
||||
|
||||
return [
|
||||
// Validation passes
|
||||
new AbstractClassPass(),
|
||||
new AssignThisVariablePass(),
|
||||
new CalledClassPass(),
|
||||
new CallTimePassByReferencePass(),
|
||||
new FinalClassPass(),
|
||||
new FunctionContextPass(),
|
||||
new FunctionReturnInWriteContextPass(),
|
||||
new IssetPass(),
|
||||
new LabelContextPass(),
|
||||
new ListPass(),
|
||||
new LoopContextPass(),
|
||||
new PassableByReferencePass(),
|
||||
new ReturnTypePass(),
|
||||
new EmptyArrayDimFetchPass(),
|
||||
new ValidConstructorPass(),
|
||||
|
||||
// Rewriting shenanigans
|
||||
...$rewritePasses,
|
||||
|
||||
// Namespace-aware validation (which depends on aforementioned shenanigans)
|
||||
new ValidClassNamePass(),
|
||||
new ValidFunctionNamePass(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* "Warm up" code cleaner passes when we're coming from a debug call.
|
||||
*
|
||||
* This sets up the alias and namespace state that `UseStatementPass` and `NamespacePass` need
|
||||
* to track between calls.
|
||||
*/
|
||||
private function addImplicitDebugContext()
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
$useStatementPass = new UseStatementPass();
|
||||
$useStatementPass->setCleaner($this);
|
||||
|
||||
$namespacePass = new NamespacePass();
|
||||
$namespacePass->setCleaner($this);
|
||||
|
||||
// Set up a clean traverser for just these code cleaner passes
|
||||
// @todo Pass visitors directly to once we drop support for PHP-Parser 4.x
|
||||
$traverser = new NodeTraverser();
|
||||
$traverser->addVisitor($useStatementPass);
|
||||
$traverser->addVisitor($namespacePass);
|
||||
|
||||
$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'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given backtrace frame is a call to Psy\debug.
|
||||
*
|
||||
* @param array $stackFrame
|
||||
*/
|
||||
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)
|
||||
{
|
||||
// Clear messages from previous clean
|
||||
$this->messages = [];
|
||||
|
||||
$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.
|
||||
*
|
||||
* TODO: switch $this->namespace over to storing ?Name at some point!
|
||||
*
|
||||
* @param Name|array|null $namespace Namespace as Name node, array of parts, or null
|
||||
*/
|
||||
public function setNamespace($namespace = null)
|
||||
{
|
||||
if ($namespace instanceof Name) {
|
||||
// Backwards compatibility shim for PHP-Parser 4.x
|
||||
$namespace = \method_exists($namespace, 'getParts') ? $namespace->getParts() : $namespace->parts;
|
||||
}
|
||||
|
||||
$this->namespace = $namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current local namespace.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getNamespace()
|
||||
{
|
||||
return $this->namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set use statement aliases for a specific namespace.
|
||||
*
|
||||
* @param Name|null $namespace Namespace name or Name node (null for global namespace)
|
||||
* @param array $aliases Map of lowercase alias names to Name nodes
|
||||
*/
|
||||
public function setAliasesForNamespace(?Name $namespace, array $aliases)
|
||||
{
|
||||
$namespaceKey = \strtolower($namespace ? $namespace->toString() : '');
|
||||
$this->aliasesByNamespace[$namespaceKey] = $aliases;
|
||||
$this->aliasesByTypeByNamespace[$namespaceKey][Use_::TYPE_NORMAL] = $aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get use statement aliases for a specific namespace.
|
||||
*
|
||||
* (This currently accepts a string namespace name, because that's all we're storing in
|
||||
* CodeCleaner as the current namespace; we should update that to be a Name node.)
|
||||
*
|
||||
* @param Name|string|null $namespace Namespace name or Name node (null for global namespace)
|
||||
*
|
||||
* @return array Map of lowercase alias names to Name nodes
|
||||
*/
|
||||
public function getAliasesForNamespace($namespace): array
|
||||
{
|
||||
$namespaceName = $namespace instanceof Name ? $namespace->toString() : $namespace;
|
||||
$namespaceKey = \strtolower($namespaceName ?? '');
|
||||
|
||||
return $this->aliasesByTypeByNamespace[$namespaceKey][Use_::TYPE_NORMAL] ?? $this->aliasesByNamespace[$namespaceKey] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set use statement aliases by import type for a specific namespace.
|
||||
*
|
||||
* @param Name|null $namespace Namespace name or Name node (null for global namespace)
|
||||
* @param array $aliasesByType Map of Use_::TYPE_* constants to alias maps
|
||||
*/
|
||||
public function setAliasesByTypeForNamespace(?Name $namespace, array $aliasesByType): void
|
||||
{
|
||||
$namespaceKey = \strtolower($namespace ? $namespace->toString() : '');
|
||||
$this->aliasesByTypeByNamespace[$namespaceKey] = $aliasesByType;
|
||||
$this->aliasesByNamespace[$namespaceKey] = $aliasesByType[Use_::TYPE_NORMAL] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get use statement aliases by import type for a specific namespace.
|
||||
*
|
||||
* @param Name|string|null $namespace Namespace name or Name node (null for global namespace)
|
||||
*
|
||||
* @return array Map of Use_::TYPE_* constants to alias maps
|
||||
*/
|
||||
public function getAliasesByTypeForNamespace($namespace): array
|
||||
{
|
||||
$namespaceName = $namespace instanceof Name ? $namespace->toString() : $namespace;
|
||||
$namespaceKey = \strtolower($namespaceName ?? '');
|
||||
|
||||
if (isset($this->aliasesByTypeByNamespace[$namespaceKey])) {
|
||||
return $this->aliasesByTypeByNamespace[$namespaceKey];
|
||||
}
|
||||
|
||||
if (!isset($this->aliasesByNamespace[$namespaceKey])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [Use_::TYPE_NORMAL => $this->aliasesByNamespace[$namespaceKey]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a class name using current use statements and namespace.
|
||||
*
|
||||
* This is used by commands to resolve short names the same way code execution does.
|
||||
* Uses PHP-Parser's NameResolver along with PsySH's custom passes.
|
||||
*
|
||||
* @param string $name Class name to resolve (e.g., "NoopChecker" or "Bar\Baz")
|
||||
*
|
||||
* @return string Resolved class name (may be FQN, or original name if no resolution found)
|
||||
*/
|
||||
public function resolveClassName(string $name): string
|
||||
{
|
||||
// Clear messages from previous resolution
|
||||
$this->messages = [];
|
||||
|
||||
// Only attempt resolution if it's a valid class name, and not already fully qualified
|
||||
if (\substr($name, 0, 1) === '\\' || !Str::isValidClassName($name)) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse as a class name constant
|
||||
$stmts = $this->parser->parse('<?php '.$name.'::class;');
|
||||
|
||||
// Create fresh passes for name resolution. They read state from $this.
|
||||
$namespacePass = new NamespacePass();
|
||||
$namespacePass->setCleaner($this);
|
||||
|
||||
$useStatementPass = new UseStatementPass();
|
||||
$useStatementPass->setCleaner($this);
|
||||
|
||||
// Create a fresh traverser with fresh passes
|
||||
$traverser = new NodeTraverser();
|
||||
$traverser->addVisitor($namespacePass);
|
||||
$traverser->addVisitor($useStatementPass);
|
||||
|
||||
// Add PHP-Parser's NameResolver - preserveOriginalNames lets us detect when resolution occurred
|
||||
$traverser->addVisitor(new NameResolver(null, [
|
||||
'preserveOriginalNames' => true,
|
||||
]));
|
||||
|
||||
// Traverse: NamespacePass wraps in namespace if needed,
|
||||
// UseStatementPass re-injects use statements,
|
||||
// PHP-Parser's NameResolver resolves to FullyQualified
|
||||
$stmts = $traverser->traverse($stmts);
|
||||
|
||||
// Find the Expression node - it might be after re-injected use statements
|
||||
// or wrapped in a Namespace_ node
|
||||
$targetStmt = null;
|
||||
foreach ($stmts as $stmt) {
|
||||
if ($stmt instanceof Namespace_) {
|
||||
// Look inside the namespace for the Expression
|
||||
foreach ($stmt->stmts ?? [] as $innerStmt) {
|
||||
if ($innerStmt instanceof Expression) {
|
||||
$targetStmt = $innerStmt;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
} elseif ($stmt instanceof Expression) {
|
||||
$targetStmt = $stmt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($targetStmt instanceof Expression) {
|
||||
$expr = $targetStmt->expr;
|
||||
if ($expr instanceof ClassConstFetch && $expr->class instanceof FullyQualified) {
|
||||
$resolved = '\\'.$expr->class->toString();
|
||||
|
||||
// Check if actual resolution occurred by comparing original to resolved
|
||||
// NameResolver preserves the original Name node in the 'originalName' attribute
|
||||
$originalName = $expr->class->getAttribute('originalName');
|
||||
|
||||
if ($originalName instanceof Name) {
|
||||
$originalStr = $originalName->toString();
|
||||
$resolvedStr = $expr->class->toString();
|
||||
|
||||
// If they differ, resolution occurred (use statement was applied)
|
||||
if ($originalStr !== $resolvedStr) {
|
||||
return $resolved;
|
||||
}
|
||||
}
|
||||
|
||||
// No transformation occurred - return original name unchanged
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Fall through to return original name
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a function name using current use statements and namespace.
|
||||
*
|
||||
* Returns the fully-qualified callable name, or null if the name would not
|
||||
* resolve to a callable function in the current REPL scope.
|
||||
*/
|
||||
public function resolveFunctionName(string $name): ?string
|
||||
{
|
||||
if ($name === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($name[0] === '\\') {
|
||||
$name = \substr($name, 1);
|
||||
|
||||
return \function_exists($name) ? '\\'.$name : null;
|
||||
}
|
||||
|
||||
$parts = \explode('\\', $name);
|
||||
$namespace = $this->getNamespace();
|
||||
$namespaceString = $namespace ? \implode('\\', $namespace) : null;
|
||||
$functionAliases = $this->getAliasesByTypeForNamespace($namespaceString)[Use_::TYPE_FUNCTION] ?? [];
|
||||
$firstPart = \strtolower($parts[0]);
|
||||
|
||||
if (isset($functionAliases[$firstPart])) {
|
||||
$aliasName = $functionAliases[$firstPart];
|
||||
// PHP-Parser 5.x uses getParts(), 4.x uses ->parts
|
||||
$aliasParts = \method_exists($aliasName, 'getParts') ? $aliasName->getParts() : $aliasName->parts;
|
||||
$resolved = \implode('\\', \array_merge($aliasParts, \array_slice($parts, 1)));
|
||||
|
||||
return \function_exists($resolved) ? '\\'.$resolved : null;
|
||||
}
|
||||
|
||||
if ($namespace) {
|
||||
$namespaced = \implode('\\', \array_merge($namespace, $parts));
|
||||
if (\function_exists($namespaced)) {
|
||||
return '\\'.$namespaced;
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($parts) > 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return \function_exists($name) ? '\\'.$name : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a direct function call from raw input in the current REPL scope.
|
||||
*
|
||||
* If $expectedName is provided, the input must be a direct call to that
|
||||
* function name in order to be considered a collision.
|
||||
*/
|
||||
public function getCallableFunctionForInput(string $input, ?string $expectedName = null): ?string
|
||||
{
|
||||
try {
|
||||
$stmts = $this->parse('<?php '.$input, false);
|
||||
} catch (ParseErrorException $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($stmts === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$call = $this->getDirectFunctionCallFromStatements($stmts, $expectedName);
|
||||
if ($call === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$function = $this->resolveFunctionName($call->name->toString());
|
||||
|
||||
if ($function !== null && $this->functionCallMatchesArity($function, $call)) {
|
||||
return $function;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first direct function call expression from a statement list.
|
||||
*
|
||||
* @param \PhpParser\Node[] $stmts
|
||||
*/
|
||||
private function getDirectFunctionCallFromStatements(array $stmts, ?string $expectedName = null): ?FuncCall
|
||||
{
|
||||
$stmt = $stmts[0] ?? null;
|
||||
if (!$stmt instanceof Expression) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$expr = $stmt->expr;
|
||||
if (!$expr instanceof FuncCall || !$expr->name instanceof Name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($expectedName !== null && \strtolower($expr->name->toString()) !== \strtolower($expectedName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $expr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a parsed function call could satisfy the target function's arity.
|
||||
*/
|
||||
private function functionCallMatchesArity(string $function, FuncCall $call): bool
|
||||
{
|
||||
try {
|
||||
$reflection = new \ReflectionFunction(\ltrim($function, '\\'));
|
||||
} catch (\ReflectionException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unpacked args can satisfy any arity, so only validate fixed arg counts.
|
||||
foreach ($call->args as $arg) {
|
||||
if ($arg->unpack) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$argCount = \count($call->args);
|
||||
$required = $reflection->getNumberOfRequiredParameters();
|
||||
$max = $reflection->isVariadic() ? \PHP_INT_MAX : $reflection->getNumberOfParameters();
|
||||
|
||||
return $argCount >= $required && $argCount <= $max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message from a CodeCleaner pass.
|
||||
*
|
||||
* @param string $message Message text to display
|
||||
*/
|
||||
public function log(string $message): void
|
||||
{
|
||||
$this->messages[] = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all logged messages from the last clean operation.
|
||||
*
|
||||
* @return string[] Array of message strings
|
||||
*/
|
||||
public function getMessages(): array
|
||||
{
|
||||
return $this->messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @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 (PhpParserError $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 (PhpParserError $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function parseErrorIsEOF(PhpParserError $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.
|
||||
*/
|
||||
private function parseErrorIsUnclosedString(PhpParserError $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(PhpParserError $e, string $code): bool
|
||||
{
|
||||
return $e->getRawMessage() === 'Unterminated comment';
|
||||
}
|
||||
|
||||
private function parseErrorIsTrailingComma(PhpParserError $e, string $code): bool
|
||||
{
|
||||
return ($e->getRawMessage() === 'A trailing comma is not allowed here') && (\substr(\rtrim($code), -1) === ',');
|
||||
}
|
||||
}
|
||||
79
vendor/psy/psysh/src/CodeCleaner/AbstractClassPass.php
vendored
Normal file
79
vendor/psy/psysh/src/CodeCleaner/AbstractClassPass.php
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt\Class_;
|
||||
use PhpParser\Node\Stmt\ClassMethod;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* The abstract class pass handles abstract classes and methods, complaining if there are too few or too many of either.
|
||||
*/
|
||||
class AbstractClassPass extends CodeCleanerPass
|
||||
{
|
||||
private Class_ $class;
|
||||
private array $abstractMethods;
|
||||
|
||||
/**
|
||||
* @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 = [];
|
||||
} elseif ($node instanceof ClassMethod) {
|
||||
if ($node->isAbstract()) {
|
||||
$name = \sprintf('%s::%s', $this->class->name, $node->name);
|
||||
$this->abstractMethods[] = $name;
|
||||
|
||||
if ($node->stmts !== null) {
|
||||
$msg = \sprintf('Abstract function %s cannot contain body', $name);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
if ($count > 0 && !$node->isAbstract()) {
|
||||
$msg = \sprintf(
|
||||
'Class %s contains %d abstract method%s must therefore be declared abstract or implement the remaining methods (%s)',
|
||||
$node->name,
|
||||
$count,
|
||||
($count === 1) ? '' : 's',
|
||||
\implode(', ', $this->abstractMethods)
|
||||
);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
43
vendor/psy/psysh/src/CodeCleaner/AssignThisVariablePass.php
vendored
Normal file
43
vendor/psy/psysh/src/CodeCleaner/AssignThisVariablePass.php
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Validate that the user input does not assign the `$this` variable.
|
||||
*
|
||||
* @author Martin Hasoň <martin.hason@gmail.com>
|
||||
*/
|
||||
class AssignThisVariablePass extends CodeCleanerPass
|
||||
{
|
||||
/**
|
||||
* Validate that the user input does not assign the `$this` variable.
|
||||
*
|
||||
* @throws 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->getStartLine());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
59
vendor/psy/psysh/src/CodeCleaner/CallTimePassByReferencePass.php
vendored
Normal file
59
vendor/psy/psysh/src/CodeCleaner/CallTimePassByReferencePass.php
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Expr\MethodCall;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PhpParser\Node\VariadicPlaceholder;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Validate that the user did not use the call-time pass-by-reference that causes a fatal error.
|
||||
*
|
||||
* As of PHP 5.4.0, call-time pass-by-reference was removed, so using it will raise a fatal error.
|
||||
*
|
||||
* @author Martin Hasoň <martin.hason@gmail.com>
|
||||
*/
|
||||
class CallTimePassByReferencePass extends CodeCleanerPass
|
||||
{
|
||||
const EXCEPTION_MESSAGE = 'Call-time pass-by-reference has been removed';
|
||||
|
||||
/**
|
||||
* Validate of use call-time pass-by-reference.
|
||||
*
|
||||
* @throws 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 (!$node instanceof FuncCall && !$node instanceof MethodCall && !$node instanceof StaticCall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($node->args as $arg) {
|
||||
if ($arg instanceof VariadicPlaceholder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($arg->byRef) {
|
||||
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
104
vendor/psy/psysh/src/CodeCleaner/CalledClassPass.php
vendored
Normal file
104
vendor/psy/psysh/src/CodeCleaner/CalledClassPass.php
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\ConstFetch;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\Stmt\Class_;
|
||||
use PhpParser\Node\Stmt\Trait_;
|
||||
use PhpParser\Node\VariadicPlaceholder;
|
||||
use Psy\Exception\ErrorException;
|
||||
|
||||
/**
|
||||
* The called class pass throws warnings for get_class() and get_called_class()
|
||||
* outside a class context.
|
||||
*/
|
||||
class CalledClassPass extends CodeCleanerPass
|
||||
{
|
||||
private bool $inClass = false;
|
||||
|
||||
/**
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->inClass = false;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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)
|
||||
{
|
||||
if ($node instanceof Class_ || $node instanceof Trait_) {
|
||||
$this->inClass = true;
|
||||
} elseif ($node instanceof FuncCall && !$this->inClass) {
|
||||
// We'll give any args at all (besides null) a pass.
|
||||
// Technically we should be checking whether the args are objects, but this will do for now.
|
||||
//
|
||||
// @todo switch this to actually validate args when we get context-aware code cleaner passes.
|
||||
if (!empty($node->args) && !$this->isNull($node->args[0])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We'll ignore name expressions as well (things like `$foo()`)
|
||||
if (!($node->name instanceof Name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$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->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|Node[]|null Replacement node (or special return value)
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Class_) {
|
||||
$this->inClass = false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isNull(Node $node): bool
|
||||
{
|
||||
if ($node instanceof VariadicPlaceholder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\property_exists($node, 'value')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $node->value instanceof ConstFetch && \strtolower($node->value->name) === 'null';
|
||||
}
|
||||
}
|
||||
22
vendor/psy/psysh/src/CodeCleaner/CodeCleanerPass.php
vendored
Normal file
22
vendor/psy/psysh/src/CodeCleaner/CodeCleanerPass.php
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\NodeVisitorAbstract;
|
||||
|
||||
/**
|
||||
* A CodeCleaner pass is a PhpParser Node Visitor.
|
||||
*/
|
||||
abstract class CodeCleanerPass extends NodeVisitorAbstract
|
||||
{
|
||||
// Wheee!
|
||||
}
|
||||
70
vendor/psy/psysh/src/CodeCleaner/EmptyArrayDimFetchPass.php
vendored
Normal file
70
vendor/psy/psysh/src/CodeCleaner/EmptyArrayDimFetchPass.php
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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 PhpParser\Node\Expr\AssignRef;
|
||||
use PhpParser\Node\Stmt\Foreach_;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Validate empty brackets are only used for assignment.
|
||||
*/
|
||||
class EmptyArrayDimFetchPass extends CodeCleanerPass
|
||||
{
|
||||
const EXCEPTION_MESSAGE = 'Cannot use [] for reading';
|
||||
|
||||
private array $theseOnesAreFine = [];
|
||||
|
||||
/**
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->theseOnesAreFine = [];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FatalErrorException if the user used 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;
|
||||
} elseif ($node instanceof AssignRef && $node->expr instanceof ArrayDimFetch) {
|
||||
$this->theseOnesAreFine[] = $node->expr;
|
||||
} elseif ($node instanceof Foreach_ && $node->valueVar instanceof ArrayDimFetch) {
|
||||
$this->theseOnesAreFine[] = $node->valueVar;
|
||||
} elseif ($node instanceof ArrayDimFetch && $node->var instanceof ArrayDimFetch) {
|
||||
// $a[]['b'] = 'c'
|
||||
if (\in_array($node, $this->theseOnesAreFine)) {
|
||||
$this->theseOnesAreFine[] = $node->var;
|
||||
}
|
||||
}
|
||||
|
||||
if ($node instanceof ArrayDimFetch && $node->dim === null) {
|
||||
if (!\in_array($node, $this->theseOnesAreFine)) {
|
||||
throw new FatalErrorException(self::EXCEPTION_MESSAGE, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
40
vendor/psy/psysh/src/CodeCleaner/ExitPass.php
vendored
Normal file
40
vendor/psy/psysh/src/CodeCleaner/ExitPass.php
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\Exit_;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
|
||||
use Psy\Exception\BreakException;
|
||||
|
||||
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_) {
|
||||
$args = $node->expr ? [new Arg($node->expr)] : [];
|
||||
|
||||
return new StaticCall(new FullyQualifiedName(BreakException::class), 'exitShell', $args);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
76
vendor/psy/psysh/src/CodeCleaner/FinalClassPass.php
vendored
Normal file
76
vendor/psy/psysh/src/CodeCleaner/FinalClassPass.php
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt\Class_;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* The final class pass handles final classes.
|
||||
*/
|
||||
class FinalClassPass extends CodeCleanerPass
|
||||
{
|
||||
private array $finalClasses = [];
|
||||
|
||||
/**
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->finalClasses = [];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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)
|
||||
{
|
||||
if ($node instanceof Class_) {
|
||||
if ($node->extends) {
|
||||
$extends = (string) $node->extends;
|
||||
if ($this->isFinalClass($extends)) {
|
||||
$msg = \sprintf('Class %s may not inherit from final class (%s)', $node->name, $extends);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
if ($node->isFinal()) {
|
||||
$this->finalClasses[\strtolower($node->name)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Class name
|
||||
*/
|
||||
private function isFinalClass(string $name): bool
|
||||
{
|
||||
if (!\class_exists($name)) {
|
||||
return isset($this->finalClasses[\strtolower($name)]);
|
||||
}
|
||||
|
||||
$refl = new \ReflectionClass($name);
|
||||
|
||||
return $refl->isFinal();
|
||||
}
|
||||
}
|
||||
73
vendor/psy/psysh/src/CodeCleaner/FunctionContextPass.php
vendored
Normal file
73
vendor/psy/psysh/src/CodeCleaner/FunctionContextPass.php
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\Yield_;
|
||||
use PhpParser\Node\FunctionLike;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
class FunctionContextPass extends CodeCleanerPass
|
||||
{
|
||||
private int $functionDepth = 0;
|
||||
|
||||
/**
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->functionDepth = 0;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof FunctionLike) {
|
||||
$this->functionDepth++;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// node is inside function context
|
||||
if ($this->functionDepth !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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->getStartLine());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 null;
|
||||
}
|
||||
}
|
||||
79
vendor/psy/psysh/src/CodeCleaner/FunctionReturnInWriteContextPass.php
vendored
Normal file
79
vendor/psy/psysh/src/CodeCleaner/FunctionReturnInWriteContextPass.php
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\Array_;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use PhpParser\Node\Expr\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;
|
||||
|
||||
/**
|
||||
* Validate that the functions are used correctly.
|
||||
*
|
||||
* @author Martin Hasoň <martin.hason@gmail.com>
|
||||
*/
|
||||
class FunctionReturnInWriteContextPass extends CodeCleanerPass
|
||||
{
|
||||
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";
|
||||
|
||||
/**
|
||||
* 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 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 instanceof VariadicPlaceholder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($item && $item->byRef && $this->isCallNode($item->value)) {
|
||||
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
} elseif ($node instanceof Isset_ || $node instanceof Unset_) {
|
||||
foreach ($node->vars as $var) {
|
||||
if (!$this->isCallNode($var)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$msg = $node instanceof Isset_ ? self::ISSET_MESSAGE : self::EXCEPTION_MESSAGE;
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
} elseif ($node instanceof Assign && $this->isCallNode($node->var)) {
|
||||
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isCallNode(Node $node): bool
|
||||
{
|
||||
return $node instanceof FuncCall || $node instanceof MethodCall || $node instanceof StaticCall;
|
||||
}
|
||||
}
|
||||
135
vendor/psy/psysh/src/CodeCleaner/ImplicitReturnPass.php
vendored
Normal file
135
vendor/psy/psysh/src/CodeCleaner/ImplicitReturnPass.php
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\Expr\Exit_;
|
||||
use PhpParser\Node\Expr\Throw_;
|
||||
use PhpParser\Node\Stmt;
|
||||
use PhpParser\Node\Stmt\Break_;
|
||||
use PhpParser\Node\Stmt\Expression;
|
||||
use PhpParser\Node\Stmt\If_;
|
||||
use PhpParser\Node\Stmt\Namespace_;
|
||||
use PhpParser\Node\Stmt\Return_;
|
||||
use PhpParser\Node\Stmt\Switch_;
|
||||
|
||||
/**
|
||||
* Add an implicit "return" to the last statement, provided it can be returned.
|
||||
*/
|
||||
class ImplicitReturnPass extends CodeCleanerPass
|
||||
{
|
||||
/**
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function beforeTraverse(array $nodes): array
|
||||
{
|
||||
return $this->addImplicitReturn($nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function addImplicitReturn(array $nodes): array
|
||||
{
|
||||
// If nodes is empty, it can't have a return value.
|
||||
if (empty($nodes)) {
|
||||
return [new Return_(NoReturnValue::create())];
|
||||
}
|
||||
|
||||
$last = \end($nodes);
|
||||
|
||||
// Special case a few types of statements to add an implicit return
|
||||
// value (even though they technically don't have any return value)
|
||||
// because showing a return value in these instances is useful and not
|
||||
// very surprising.
|
||||
if ($last instanceof If_) {
|
||||
$last->stmts = $this->addImplicitReturn($last->stmts);
|
||||
|
||||
foreach ($last->elseifs as $elseif) {
|
||||
$elseif->stmts = $this->addImplicitReturn($elseif->stmts);
|
||||
}
|
||||
|
||||
if ($last->else) {
|
||||
$last->else->stmts = $this->addImplicitReturn($last->else->stmts);
|
||||
}
|
||||
} elseif ($last instanceof Switch_) {
|
||||
foreach ($last->cases as $case) {
|
||||
// only add an implicit return to cases which end in break
|
||||
$caseLast = \end($case->stmts);
|
||||
if ($caseLast instanceof Break_) {
|
||||
$case->stmts = $this->addImplicitReturn(\array_slice($case->stmts, 0, -1));
|
||||
$case->stmts[] = $caseLast;
|
||||
}
|
||||
}
|
||||
} elseif ($last instanceof Expr && !($last instanceof Exit_) && !self::isThrowNode($last)) {
|
||||
// @codeCoverageIgnoreStart
|
||||
$nodes[\count($nodes) - 1] = new Return_($last, [
|
||||
'startLine' => $last->getStartLine(),
|
||||
'endLine' => $last->getEndLine(),
|
||||
]);
|
||||
// @codeCoverageIgnoreEnd
|
||||
} elseif ($last instanceof Expression && !($last->expr instanceof Exit_) && !self::isThrowNode($last->expr)) {
|
||||
$nodes[\count($nodes) - 1] = new Return_($last->expr, [
|
||||
'startLine' => $last->getStartLine(),
|
||||
'endLine' => $last->getEndLine(),
|
||||
]);
|
||||
} elseif ($last instanceof Namespace_) {
|
||||
$last->stmts = $this->addImplicitReturn($last->stmts);
|
||||
}
|
||||
|
||||
// Return a "no return value" for all non-expression statements, so that
|
||||
// PsySH can suppress the `null` that `eval()` returns otherwise.
|
||||
//
|
||||
// Note that statements special cased above (if/elseif/else, switch)
|
||||
// _might_ implicitly return a value before this catch-all return is
|
||||
// reached.
|
||||
//
|
||||
// We're not adding a fallback return after namespace statements,
|
||||
// because code outside namespace statements doesn't really work, and
|
||||
// there's already an implicit return in the namespace statement anyway.
|
||||
if (self::isNonExpressionStmt($last) && !self::isThrowNode($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
|
||||
*/
|
||||
private static function isNonExpressionStmt(Node $node): bool
|
||||
{
|
||||
return $node instanceof Stmt &&
|
||||
!$node instanceof Expression &&
|
||||
!$node instanceof Return_ &&
|
||||
!$node instanceof Namespace_;
|
||||
}
|
||||
|
||||
/**
|
||||
* PHP-Parser 4.x modeled standalone `throw` as Stmt\Throw_, while newer
|
||||
* versions expose it as Expr\Throw_ inside Stmt\Expression.
|
||||
*/
|
||||
private static function isThrowNode(Node $node): bool
|
||||
{
|
||||
return $node instanceof Throw_ || $node->getType() === 'Stmt_Throw';
|
||||
}
|
||||
}
|
||||
394
vendor/psy/psysh/src/CodeCleaner/ImplicitUsePass.php
vendored
Normal file
394
vendor/psy/psysh/src/CodeCleaner/ImplicitUsePass.php
vendored
Normal file
@@ -0,0 +1,394 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
|
||||
use PhpParser\Node\Stmt\GroupUse;
|
||||
use PhpParser\Node\Stmt\Namespace_;
|
||||
use PhpParser\Node\Stmt\Use_;
|
||||
use PhpParser\Node\Stmt\UseItem;
|
||||
use PhpParser\Node\Stmt\UseUse;
|
||||
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
||||
use Psy\CodeCleaner;
|
||||
|
||||
/**
|
||||
* Automatically add use statements for unqualified class references.
|
||||
*
|
||||
* When a user references a class by its short name (e.g., `User`), this pass attempts to find a
|
||||
* fully-qualified class name that matches. A use statement is added if:
|
||||
*
|
||||
* - There is no unqualified name (class/function/constant) with that short name
|
||||
* - There is no existing use statement or alias with that short name
|
||||
* - There is exactly one matching class/interface/trait in the configured namespaces
|
||||
*
|
||||
* For example, in a project with `App\Model\User` and `App\View\User` classes, if configured with
|
||||
* 'includeNamespaces' => ['App\Model'], `new User` would become `use App\Model\User; new User;`
|
||||
* even though there's also an `App\View\User` class.
|
||||
*
|
||||
* Works great with autoload warming (--warm-autoload) to pre-load classes.
|
||||
*/
|
||||
class ImplicitUsePass extends CodeCleanerPass
|
||||
{
|
||||
private ?array $shortNameMap = null;
|
||||
private array $implicitUses = [];
|
||||
private array $seenNames = [];
|
||||
private array $existingAliases = [];
|
||||
private array $includeNamespaces = [];
|
||||
private array $excludeNamespaces = [];
|
||||
private ?string $currentNamespace = null;
|
||||
private ?CodeCleaner $cleaner = null;
|
||||
private ?PrettyPrinter $printer = null;
|
||||
|
||||
/**
|
||||
* @param array $config Configuration array with 'includeNamespaces' and/or 'excludeNamespaces'
|
||||
* @param CodeCleaner|null $cleaner CodeCleaner instance for logging
|
||||
*/
|
||||
public function __construct(array $config = [], ?CodeCleaner $cleaner = null)
|
||||
{
|
||||
$this->includeNamespaces = $this->normalizeNamespaces($config['includeNamespaces'] ?? []);
|
||||
$this->excludeNamespaces = $this->normalizeNamespaces($config['excludeNamespaces'] ?? []);
|
||||
$this->cleaner = $cleaner;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
if (empty($this->includeNamespaces) && empty($this->excludeNamespaces)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->buildShortNameMap();
|
||||
|
||||
// Reset state for this traversal
|
||||
$this->implicitUses = [];
|
||||
$this->seenNames = [];
|
||||
$this->existingAliases = [];
|
||||
$this->currentNamespace = null;
|
||||
|
||||
$modified = false;
|
||||
|
||||
// Collect use statements and seen names for each namespace
|
||||
foreach ($nodes as $node) {
|
||||
if ($node instanceof Namespace_) {
|
||||
$this->currentNamespace = $node->name ? $node->name->toString() : null;
|
||||
|
||||
$perNamespaceAliases = [];
|
||||
$perNamespaceUses = [];
|
||||
$perNamespaceSeen = [];
|
||||
|
||||
if ($node->stmts !== null) {
|
||||
$this->collectAliasesInNodes($node->stmts, $perNamespaceAliases);
|
||||
$this->collectNamesInNodes($node->stmts, $perNamespaceSeen, $perNamespaceAliases, $perNamespaceUses);
|
||||
}
|
||||
|
||||
if (!empty($perNamespaceUses)) {
|
||||
$this->logAddedUses($perNamespaceUses);
|
||||
$node->stmts = \array_merge($this->createUseStatements($perNamespaceUses), $node->stmts ?? []);
|
||||
$modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$hasNamespace = false;
|
||||
foreach ($nodes as $node) {
|
||||
if ($node instanceof Namespace_) {
|
||||
$hasNamespace = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect use statements and seen names for top-level namespace
|
||||
if (!$hasNamespace) {
|
||||
$this->currentNamespace = null;
|
||||
$topLevelAliases = [];
|
||||
$topLevelUses = [];
|
||||
$topLevelSeen = [];
|
||||
|
||||
$this->collectAliasesInNodes($nodes, $topLevelAliases);
|
||||
$this->collectNamesInNodes($nodes, $topLevelSeen, $topLevelAliases, $topLevelUses);
|
||||
|
||||
if (!empty($topLevelUses)) {
|
||||
$this->logAddedUses($topLevelUses);
|
||||
|
||||
return \array_merge($this->createUseStatements($topLevelUses), $nodes);
|
||||
}
|
||||
}
|
||||
|
||||
return $modified ? $nodes : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect aliases in a set of nodes.
|
||||
*
|
||||
* @param array $nodes Array of Node objects
|
||||
* @param array $aliases Associative array mapping lowercase alias names to true
|
||||
*/
|
||||
private function collectAliasesInNodes(array $nodes, array &$aliases): void
|
||||
{
|
||||
foreach ($nodes as $node) {
|
||||
if ($node instanceof Use_ || $node instanceof GroupUse) {
|
||||
foreach ($node->uses as $useItem) {
|
||||
$alias = $useItem->getAlias();
|
||||
if ($alias !== null) {
|
||||
$aliasStr = $alias instanceof Name ? $alias->toString() : (string) $alias;
|
||||
$aliases[\strtolower($aliasStr)] = true;
|
||||
} else {
|
||||
$aliases[\strtolower($this->getShortName($useItem->name))] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect unqualified names in nodes.
|
||||
*
|
||||
* @param array $nodes Array of Node objects to traverse
|
||||
* @param array $seen Lowercase short names already processed
|
||||
* @param array $aliases Lowercase alias names that exist in this namespace
|
||||
* @param array $uses Map of short names to FQNs for implicit use statements
|
||||
*/
|
||||
private function collectNamesInNodes(array $nodes, array &$seen, array $aliases, array &$uses): void
|
||||
{
|
||||
foreach ($nodes as $node) {
|
||||
if (!$node instanceof Node || $node instanceof Use_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($node instanceof Name && !$node instanceof FullyQualifiedName) {
|
||||
if (!$this->isQualified($node)) {
|
||||
$shortName = $this->getShortName($node);
|
||||
$shortNameLower = \strtolower($shortName);
|
||||
|
||||
if (isset($seen[$shortNameLower])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen[$shortNameLower] = true;
|
||||
|
||||
if ($this->shouldAddImplicitUseInContext($shortName, $shortNameLower, $aliases)) {
|
||||
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable - shortNameMap is initialized in beforeTraverse
|
||||
$uses[$shortName] = $this->shortNameMap[$shortNameLower];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($node->getSubNodeNames() as $subNodeName) {
|
||||
$subNode = $node->$subNodeName;
|
||||
if ($subNode instanceof Node) {
|
||||
$subNode = [$subNode];
|
||||
}
|
||||
|
||||
if (\is_array($subNode)) {
|
||||
$this->collectNamesInNodes($subNode, $seen, $aliases, $uses);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Use_ statement nodes from uses array.
|
||||
*
|
||||
* @param array $uses Associative array mapping short names to FQNs
|
||||
*
|
||||
* @return Use_[]
|
||||
*/
|
||||
private function createUseStatements(array $uses): array
|
||||
{
|
||||
\asort($uses);
|
||||
|
||||
$useStatements = [];
|
||||
foreach ($uses as $fqn) {
|
||||
$useItem = \class_exists(UseItem::class) ? new UseItem(new Name($fqn)) : new UseUse(new Name($fqn));
|
||||
$useStatements[] = new Use_([$useItem]);
|
||||
}
|
||||
|
||||
return $useStatements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should add an implicit use statement for this name in current context.
|
||||
*
|
||||
* @param string $shortName Original case short name
|
||||
* @param string $shortNameLower Lowercase short name for comparison
|
||||
* @param array $aliases Lowercase alias names that exist in this namespace
|
||||
*/
|
||||
private function shouldAddImplicitUseInContext(string $shortName, string $shortNameLower, array $aliases): bool
|
||||
{
|
||||
// Rule 1: No existing unqualified name (class/interface/trait) with that short name
|
||||
if (\class_exists($shortName, false) || \interface_exists($shortName, false) || \trait_exists($shortName, false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rule 2: No existing use statement or alias with that short name
|
||||
if (isset($aliases[$shortNameLower])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rule 3: Exactly one matching short class/interface/trait in configured namespaces
|
||||
if (!isset($this->shortNameMap[$shortNameLower]) || $this->shortNameMap[$shortNameLower] === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rule 4: Don't add use statement if the class exists in the current namespace
|
||||
if ($this->currentNamespace !== null) {
|
||||
$expectedFqn = \trim($this->currentNamespace, '\\').'\\'.$shortName;
|
||||
|
||||
if (\class_exists($expectedFqn, false) || \interface_exists($expectedFqn, false) || \trait_exists($expectedFqn, false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a map of short class names to fully-qualified names.
|
||||
*
|
||||
* Uses get_declared_classes(), get_declared_interfaces(), and get_declared_traits()
|
||||
* to find all currently loaded classes. Only includes classes matching the configured
|
||||
* namespace filters. Detects ambiguous short names (multiple FQNs with same short name
|
||||
* within the filtered namespaces) and marks them as null.
|
||||
*/
|
||||
private function buildShortNameMap(): void
|
||||
{
|
||||
$this->shortNameMap = [];
|
||||
|
||||
$allClasses = [
|
||||
...\get_declared_classes(),
|
||||
...\get_declared_interfaces(),
|
||||
...\get_declared_traits(),
|
||||
];
|
||||
|
||||
// First pass: collect all matching classes
|
||||
$candidatesByShortName = [];
|
||||
foreach ($allClasses as $fqn) {
|
||||
if (!$this->shouldIncludeClass($fqn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = \explode('\\', $fqn);
|
||||
$shortName = \strtolower(\end($parts));
|
||||
|
||||
if (!isset($candidatesByShortName[$shortName])) {
|
||||
$candidatesByShortName[$shortName] = [];
|
||||
}
|
||||
$candidatesByShortName[$shortName][] = $fqn;
|
||||
}
|
||||
|
||||
// Second pass: determine if each short name is unique or ambiguous
|
||||
foreach ($candidatesByShortName as $shortName => $fqns) {
|
||||
$uniqueFqns = \array_unique($fqns);
|
||||
// Mark as null if ambiguous (multiple FQNs with same short name)
|
||||
$this->shortNameMap[$shortName] = (\count($uniqueFqns) === 1) ? $uniqueFqns[0] : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a class should be aliased based on namespace filters.
|
||||
*
|
||||
* @param string $fqn Fully-qualified class name
|
||||
*/
|
||||
private function shouldIncludeClass(string $fqn): bool
|
||||
{
|
||||
if (\strpos($fqn, '\\') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($this->includeNamespaces) && empty($this->excludeNamespaces)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->excludeNamespaces as $namespace) {
|
||||
if (\stripos($fqn, $namespace) === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->includeNamespaces)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->includeNamespaces as $namespace) {
|
||||
if (\stripos($fqn, $namespace) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize namespace prefixes.
|
||||
*
|
||||
* Removes leading backslash and ensures trailing backslash.
|
||||
*
|
||||
* @param string[] $namespaces
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function normalizeNamespaces(array $namespaces): array
|
||||
{
|
||||
return \array_map(fn ($namespace) => \trim($namespace, '\\').'\\', $namespaces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get short name from a Name node.
|
||||
*/
|
||||
private function getShortName(Name $name): string
|
||||
{
|
||||
$parts = $this->getParts($name);
|
||||
|
||||
return \end($parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a name is qualified (contains namespace separator).
|
||||
*/
|
||||
private function isQualified(Name $name): bool
|
||||
{
|
||||
return \count($this->getParts($name)) > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backwards compatibility shim for PHP-Parser 4.x.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function getParts(Name $name): array
|
||||
{
|
||||
return \method_exists($name, 'getParts') ? $name->getParts() : $name->parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log added use statements to the CodeCleaner.
|
||||
*
|
||||
* @param array $uses Associative array mapping short names to FQNs
|
||||
*/
|
||||
private function logAddedUses(array $uses): void
|
||||
{
|
||||
if ($this->cleaner === null || empty($uses)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->printer === null) {
|
||||
$this->printer = new PrettyPrinter();
|
||||
}
|
||||
|
||||
$useStmts = $this->createUseStatements($uses);
|
||||
$this->cleaner->log($this->printer->prettyPrint($useStmts));
|
||||
}
|
||||
}
|
||||
51
vendor/psy/psysh/src/CodeCleaner/IssetPass.php
vendored
Normal file
51
vendor/psy/psysh/src/CodeCleaner/IssetPass.php
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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 null;
|
||||
}
|
||||
|
||||
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->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
105
vendor/psy/psysh/src/CodeCleaner/LabelContextPass.php
vendored
Normal file
105
vendor/psy/psysh/src/CodeCleaner/LabelContextPass.php
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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
|
||||
{
|
||||
private int $functionDepth = 0;
|
||||
private array $labelDeclarations = [];
|
||||
private array $labelGotos = [];
|
||||
|
||||
/**
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->functionDepth = 0;
|
||||
$this->labelDeclarations = [];
|
||||
$this->labelGotos = [];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof FunctionLike) {
|
||||
$this->functionDepth++;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// node is inside function context
|
||||
if ($this->functionDepth !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($node instanceof Goto_) {
|
||||
$this->labelGotos[\strtolower($node->name)] = $node->getStartLine();
|
||||
} elseif ($node instanceof Label) {
|
||||
$this->labelDeclarations[\strtolower($node->name)] = $node->getStartLine();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
40
vendor/psy/psysh/src/CodeCleaner/LeavePsyshAlonePass.php
vendored
Normal file
40
vendor/psy/psysh/src/CodeCleaner/LeavePsyshAlonePass.php
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use Psy\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* Validate that the user input does not reference the `$__psysh__` variable.
|
||||
*/
|
||||
class LeavePsyshAlonePass extends CodeCleanerPass
|
||||
{
|
||||
/**
|
||||
* Validate that the user input does not reference the `$__psysh__` variable.
|
||||
*
|
||||
* @throws RuntimeException if the user is messing with $__psysh__
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @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');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
97
vendor/psy/psysh/src/CodeCleaner/ListPass.php
vendored
Normal file
97
vendor/psy/psysh/src/CodeCleaner/ListPass.php
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\ArrayItem;
|
||||
use PhpParser\Node\Expr\Array_;
|
||||
use PhpParser\Node\Expr\ArrayDimFetch;
|
||||
// @todo Drop PhpParser\Node\Expr\ArrayItem once we drop support for PHP-Parser 4.x
|
||||
use PhpParser\Node\Expr\ArrayItem as LegacyArrayItem;
|
||||
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
|
||||
{
|
||||
/**
|
||||
* 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 null;
|
||||
}
|
||||
|
||||
if (!$node->var instanceof Array_ && !$node->var instanceof List_) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Polyfill for PHP-Parser 2.x
|
||||
$items = isset($node->var->items) ? $node->var->items : (\property_exists($node->var, 'vars') ? $node->var->vars : []);
|
||||
|
||||
if ($items === [] || $items === [null]) {
|
||||
throw new ParseErrorException('Cannot use empty list', ['startLine' => $node->var->getStartLine(), 'endLine' => $node->var->getEndLine()]);
|
||||
}
|
||||
|
||||
$itemFound = false;
|
||||
foreach ($items as $item) {
|
||||
if ($item === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$itemFound = true;
|
||||
|
||||
if (!self::isValidArrayItem($item)) {
|
||||
$msg = 'Assignments can only happen to writable values';
|
||||
throw new ParseErrorException($msg, ['startLine' => $item->getStartLine(), 'endLine' => $item->getEndLine()]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$itemFound) {
|
||||
throw new ParseErrorException('Cannot use empty list');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether a given item in an array is valid for short assignment.
|
||||
*
|
||||
* @param Node $item
|
||||
*/
|
||||
private static function isValidArrayItem(Node $item): bool
|
||||
{
|
||||
$value = ($item instanceof ArrayItem || $item instanceof LegacyArrayItem) ? $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;
|
||||
}
|
||||
}
|
||||
123
vendor/psy/psysh/src/CodeCleaner/LoopContextPass.php
vendored
Normal file
123
vendor/psy/psysh/src/CodeCleaner/LoopContextPass.php
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Scalar\DNumber;
|
||||
use PhpParser\Node\Scalar\Float_;
|
||||
use PhpParser\Node\Scalar\Int_;
|
||||
use PhpParser\Node\Scalar\LNumber;
|
||||
use PhpParser\Node\Stmt\Break_;
|
||||
use PhpParser\Node\Stmt\Continue_;
|
||||
use PhpParser\Node\Stmt\Do_;
|
||||
use PhpParser\Node\Stmt\For_;
|
||||
use PhpParser\Node\Stmt\Foreach_;
|
||||
use PhpParser\Node\Stmt\Switch_;
|
||||
use PhpParser\Node\Stmt\While_;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* The loop context pass handles invalid `break` and `continue` statements.
|
||||
*/
|
||||
class LoopContextPass extends CodeCleanerPass
|
||||
{
|
||||
private int $loopDepth = 0;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->loopDepth = 0;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FatalErrorException if the node is a break or continue in a non-loop or switch context
|
||||
* @throws FatalErrorException if the node is trying to break out of more nested structures than exist
|
||||
* @throws FatalErrorException if the node is a break or continue and has a non-numeric argument
|
||||
* @throws FatalErrorException if the node is a break or continue and has an argument less than 1
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
switch (true) {
|
||||
case $node instanceof Do_:
|
||||
case $node instanceof For_:
|
||||
case $node instanceof Foreach_:
|
||||
case $node instanceof Switch_:
|
||||
case $node instanceof While_:
|
||||
$this->loopDepth++;
|
||||
break;
|
||||
|
||||
case $node instanceof Break_:
|
||||
case $node instanceof Continue_:
|
||||
$operator = $node instanceof Break_ ? 'break' : 'continue';
|
||||
|
||||
if ($this->loopDepth === 0) {
|
||||
$msg = \sprintf("'%s' not in the 'loop' or 'switch' context", $operator);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
|
||||
// @todo Remove LNumber and DNumber once we drop support for PHP-Parser 4.x
|
||||
if (
|
||||
$node->num instanceof LNumber ||
|
||||
$node->num instanceof DNumber ||
|
||||
$node->num instanceof Int_ ||
|
||||
$node->num instanceof Float_
|
||||
) {
|
||||
$num = $node->num->value;
|
||||
if ($node->num instanceof DNumber || $num < 1) {
|
||||
$msg = \sprintf("'%s' operator accepts only positive numbers", $operator);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
|
||||
if ($num > $this->loopDepth) {
|
||||
$msg = \sprintf("Cannot '%s' %d levels", $operator, $num);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
} 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->getStartLine());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|Node[]|null Replacement node (or special return value)
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
switch (true) {
|
||||
case $node instanceof Do_:
|
||||
case $node instanceof For_:
|
||||
case $node instanceof Foreach_:
|
||||
case $node instanceof Switch_:
|
||||
case $node instanceof While_:
|
||||
$this->loopDepth--;
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
44
vendor/psy/psysh/src/CodeCleaner/MagicConstantsPass.php
vendored
Normal file
44
vendor/psy/psysh/src/CodeCleaner/MagicConstantsPass.php
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\Scalar\MagicConst\Dir;
|
||||
use PhpParser\Node\Scalar\MagicConst\File;
|
||||
use PhpParser\Node\Scalar\String_;
|
||||
|
||||
/**
|
||||
* Swap out __DIR__ and __FILE__ magic constants with our best guess?
|
||||
*/
|
||||
class MagicConstantsPass extends CodeCleanerPass
|
||||
{
|
||||
/**
|
||||
* Swap out __DIR__ and __FILE__ constants, because the default ones when
|
||||
* calling eval() don't make sense.
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return FuncCall|String_|null
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Dir) {
|
||||
return new FuncCall(new Name('getcwd'), [], $node->getAttributes());
|
||||
} elseif ($node instanceof File) {
|
||||
return new String_('', $node->getAttributes());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
228
vendor/psy/psysh/src/CodeCleaner/NamespaceAwarePass.php
vendored
Normal file
228
vendor/psy/psysh/src/CodeCleaner/NamespaceAwarePass.php
vendored
Normal file
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
|
||||
use PhpParser\Node\Stmt\GroupUse;
|
||||
use PhpParser\Node\Stmt\Namespace_;
|
||||
use PhpParser\Node\Stmt\Use_;
|
||||
use Psy\CodeCleaner;
|
||||
|
||||
/**
|
||||
* Abstract namespace-aware code cleaner pass.
|
||||
*
|
||||
* Tracks both namespace and use statement aliases for proper name resolution.
|
||||
*/
|
||||
abstract class NamespaceAwarePass extends CodeCleanerPass
|
||||
{
|
||||
protected array $namespace = [];
|
||||
protected array $currentScope = [];
|
||||
protected array $aliases = [];
|
||||
protected array $aliasesByType = [];
|
||||
protected ?CodeCleaner $cleaner = null;
|
||||
|
||||
/**
|
||||
* Set the CodeCleaner instance for state management.
|
||||
*/
|
||||
public function setCleaner(CodeCleaner $cleaner)
|
||||
{
|
||||
$this->cleaner = $cleaner;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo should this be final? Extending classes should be sure to either
|
||||
* use afterTraverse or call parent::beforeTraverse() when overloading.
|
||||
*
|
||||
* Reset the namespace and the current scope before beginning analysis
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->namespace = [];
|
||||
$this->currentScope = [];
|
||||
$this->aliasesByType = [];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo should this be final? Extending classes should be sure to either use
|
||||
* 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) ? $this->getParts($node->name) : [];
|
||||
|
||||
// Only restore use statement aliases for PsySH re-injected namespaces.
|
||||
// Explicit namespace declarations start with a clean slate.
|
||||
if ($this->cleaner && $node->getAttribute('psyshReinjected')) {
|
||||
$this->aliasesByType = $this->cleaner->getAliasesByTypeForNamespace($node->name);
|
||||
$this->aliases = $this->aliasesByType[Use_::TYPE_NORMAL] ?? [];
|
||||
} else {
|
||||
$this->aliases = [];
|
||||
$this->aliasesByType = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Track use statements for alias resolution
|
||||
if ($node instanceof Use_) {
|
||||
foreach ($node->uses as $useItem) {
|
||||
$this->setAliasForType(\strtolower($useItem->getAlias()), $useItem->name, $this->getUseImportType($node, $useItem));
|
||||
}
|
||||
}
|
||||
|
||||
// Track group use statements
|
||||
if ($node instanceof GroupUse) {
|
||||
foreach ($node->uses as $useItem) {
|
||||
$this->setAliasForType(\strtolower($useItem->getAlias()), Name::concat($node->prefix, $useItem->name), $this->getUseImportType($node, $useItem));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save alias state when leaving a namespace.
|
||||
*
|
||||
* Braced namespaces (like `namespace { ... }`) are self-contained and don't persist their use
|
||||
* statements between executions.
|
||||
*
|
||||
* Only save aliases for open namespaces (like `namespace Foo;`), or implicit namespace wrappers
|
||||
* re-injected by PsySH (psyshReinjected).
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Namespace_) {
|
||||
$this->syncCompatAliases();
|
||||
|
||||
// Open namespaces (like `namespace Foo;`) have kind == KIND_SEMICOLON.
|
||||
if ($node->getAttribute('kind') === Namespace_::KIND_SEMICOLON || $node->getAttribute('psyshReinjected')) {
|
||||
if ($this->cleaner) {
|
||||
$this->cleaner->setAliasesByTypeForNamespace($node->name, $this->aliasesByType);
|
||||
}
|
||||
}
|
||||
|
||||
$this->aliases = [];
|
||||
$this->aliasesByType = [];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a fully-qualified name (class, function, interface, etc).
|
||||
*
|
||||
* Resolves use statement aliases before applying namespace.
|
||||
*
|
||||
* @param mixed $name
|
||||
*/
|
||||
protected function getFullyQualifiedName($name): string
|
||||
{
|
||||
$this->syncCompatAliases();
|
||||
|
||||
if ($name instanceof FullyQualifiedName) {
|
||||
return \implode('\\', $this->getParts($name));
|
||||
}
|
||||
|
||||
// Check if this name matches a use statement alias
|
||||
if ($name instanceof Name) {
|
||||
$nameParts = $this->getParts($name);
|
||||
$firstPart = \strtolower($nameParts[0]);
|
||||
|
||||
if (isset($this->aliases[$firstPart])) {
|
||||
// Replace first part with the aliased namespace
|
||||
$aliasedParts = $this->getParts($this->aliases[$firstPart]);
|
||||
\array_shift($nameParts); // Remove first part
|
||||
|
||||
return \implode('\\', \array_merge($aliasedParts, $nameParts));
|
||||
}
|
||||
}
|
||||
|
||||
if ($name instanceof Name) {
|
||||
$name = $this->getParts($name);
|
||||
} elseif (!\is_array($name)) {
|
||||
$name = [$name];
|
||||
}
|
||||
|
||||
return \implode('\\', \array_merge($this->namespace, $name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Backwards compatibility shim for PHP-Parser 4.x.
|
||||
*
|
||||
* At some point we might want to make $namespace a plain string, to match how Name works?
|
||||
*/
|
||||
protected function getParts(Name $name): array
|
||||
{
|
||||
return \method_exists($name, 'getParts') ? $name->getParts() : $name->parts;
|
||||
}
|
||||
|
||||
protected function getAliasesForType(int $type): array
|
||||
{
|
||||
$this->syncCompatAliases();
|
||||
|
||||
return $this->aliasesByType[$type] ?? [];
|
||||
}
|
||||
|
||||
private function setAliasForType(string $alias, Name $name, int $type): void
|
||||
{
|
||||
$this->aliasesByType[$type][$alias] = $name;
|
||||
if ($type === Use_::TYPE_NORMAL) {
|
||||
$this->aliases[$alias] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync $aliases into $aliasesByType[TYPE_NORMAL] for subclasses that read $aliases directly.
|
||||
*/
|
||||
private function syncCompatAliases(): void
|
||||
{
|
||||
if ($this->aliases === []) {
|
||||
unset($this->aliasesByType[Use_::TYPE_NORMAL]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->aliasesByType[Use_::TYPE_NORMAL] = $this->aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the import type for a use item across PHP-Parser 4.x and 5.x.
|
||||
*
|
||||
* Individual use items may specify their own type (e.g. in group use
|
||||
* statements), otherwise fall back to the parent statement type.
|
||||
*/
|
||||
protected function getUseImportType(Node $node, Node $useItem): int
|
||||
{
|
||||
$itemType = $useItem->type ?? null;
|
||||
if (\is_int($itemType) && $itemType !== Use_::TYPE_UNKNOWN) {
|
||||
return $itemType;
|
||||
}
|
||||
|
||||
$nodeType = $node->type ?? null;
|
||||
if (\is_int($nodeType) && $nodeType !== Use_::TYPE_UNKNOWN) {
|
||||
return $nodeType;
|
||||
}
|
||||
|
||||
return Use_::TYPE_NORMAL;
|
||||
}
|
||||
}
|
||||
119
vendor/psy/psysh/src/CodeCleaner/NamespacePass.php
vendored
Normal file
119
vendor/psy/psysh/src/CodeCleaner/NamespacePass.php
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\Stmt\Namespace_;
|
||||
use Psy\CodeCleaner;
|
||||
|
||||
/**
|
||||
* Provide implicit namespaces for subsequent execution.
|
||||
*
|
||||
* The namespace pass remembers the last standalone namespace line encountered:
|
||||
*
|
||||
* namespace Foo\Bar;
|
||||
*
|
||||
* ... which it then applies implicitly to all future evaluated code, until the
|
||||
* namespace is replaced by another namespace. To reset to the top level
|
||||
* namespace, enter `namespace {}`. This is a bit ugly, but it does the trick :)
|
||||
*/
|
||||
class NamespacePass extends NamespaceAwarePass
|
||||
{
|
||||
/**
|
||||
* @param ?CodeCleaner $cleaner deprecated parameter, use setCleaner() instead
|
||||
*
|
||||
* @phpstan-ignore-next-line method.unused
|
||||
*/
|
||||
public function __construct(?CodeCleaner $cleaner = null)
|
||||
{
|
||||
// No-op, since cleaner is provided by NamespaceAwarePass
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is a standalone namespace line, remember it for later.
|
||||
*
|
||||
* Otherwise, apply remembered namespaces to the code until a new namespace
|
||||
* is encountered.
|
||||
*
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
if (empty($nodes)) {
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
$last = \end($nodes);
|
||||
|
||||
if ($last instanceof Namespace_) {
|
||||
$kind = $last->getAttribute('kind');
|
||||
|
||||
if ($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;
|
||||
}
|
||||
|
||||
// Wrap in current namespace if one is set
|
||||
$currentNamespace = $this->getCurrentNamespace();
|
||||
|
||||
if (!$currentNamespace) {
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
// Mark as re-injected so UseStatementPass knows it can re-inject use statements
|
||||
return [new Namespace_($currentNamespace, $nodes, ['psyshReinjected' => true])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current namespace as a Name node.
|
||||
*
|
||||
* This is more complicated than it needs to be, because we're not storing namespace as a Name.
|
||||
*
|
||||
* @return Name|null
|
||||
*/
|
||||
private function getCurrentNamespace(): ?Name
|
||||
{
|
||||
$namespace = $this->cleaner->getNamespace();
|
||||
|
||||
return $namespace ? new Name($namespace) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the namespace in CodeCleaner and clear aliases.
|
||||
*
|
||||
* @param Name|null $namespace
|
||||
*/
|
||||
private function setNamespace(?Name $namespace)
|
||||
{
|
||||
$this->cleaner->setNamespace($namespace);
|
||||
|
||||
// Always clear aliases when changing namespace
|
||||
$this->cleaner->setAliasesByTypeForNamespace($namespace, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated unused and will be removed in a future version
|
||||
*/
|
||||
protected function getParts(Name $name): array
|
||||
{
|
||||
return \method_exists($name, 'getParts') ? $name->getParts() : $name->parts;
|
||||
}
|
||||
}
|
||||
33
vendor/psy/psysh/src/CodeCleaner/NoReturnValue.php
vendored
Normal file
33
vendor/psy/psysh/src/CodeCleaner/NoReturnValue.php
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node\Expr\New_;
|
||||
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
|
||||
|
||||
/**
|
||||
* A class used internally by CodeCleaner to represent input, such as
|
||||
* non-expression statements, with no return value.
|
||||
*
|
||||
* Note that user code returning an instance of this class will act like it
|
||||
* has no return value, so you prolly shouldn't do that.
|
||||
*/
|
||||
class NoReturnValue
|
||||
{
|
||||
/**
|
||||
* Get PhpParser AST expression for creating a new NoReturnValue.
|
||||
*/
|
||||
public static function create(): New_
|
||||
{
|
||||
return new New_(new FullyQualifiedName(self::class));
|
||||
}
|
||||
}
|
||||
137
vendor/psy/psysh/src/CodeCleaner/PassableByReferencePass.php
vendored
Normal file
137
vendor/psy/psysh/src/CodeCleaner/PassableByReferencePass.php
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\ClassConstFetch;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Expr\MethodCall;
|
||||
use PhpParser\Node\Expr\PropertyFetch;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use PhpParser\Node\VariadicPlaceholder;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Validate that only variables (and variable-like things) are passed by reference.
|
||||
*/
|
||||
class PassableByReferencePass extends CodeCleanerPass
|
||||
{
|
||||
const EXCEPTION_MESSAGE = 'Only variables can be passed by reference';
|
||||
|
||||
/**
|
||||
* @throws FatalErrorException if non-variables are passed by reference
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
// @todo support MethodCall and StaticCall as well.
|
||||
if ($node instanceof FuncCall) {
|
||||
// if function name is an expression or a variable, give it a pass for now.
|
||||
if ($node->name instanceof Expr || $node->name instanceof Variable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = (string) $node->name;
|
||||
|
||||
if ($name === 'array_multisort') {
|
||||
return $this->validateArrayMultisort($node);
|
||||
}
|
||||
|
||||
try {
|
||||
$refl = new \ReflectionFunction($name);
|
||||
} catch (\ReflectionException $e) {
|
||||
// Well, we gave it a shot!
|
||||
return null;
|
||||
}
|
||||
|
||||
$args = [];
|
||||
foreach ($node->args as $position => $arg) {
|
||||
if ($arg instanceof VariadicPlaceholder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Named arguments were added in php-parser 4.1, so we need to check if the property exists
|
||||
$key = (\property_exists($arg, 'name') && $arg->name !== null) ? $arg->name->name : $position;
|
||||
$args[$key] = $arg;
|
||||
}
|
||||
|
||||
foreach ($refl->getParameters() as $key => $param) {
|
||||
if (\array_key_exists($key, $args) || \array_key_exists($param->name, $args)) {
|
||||
$arg = $args[$param->name] ?? $args[$key];
|
||||
if ($param->isPassedByReference() && !$this->isPassableByReference($arg)) {
|
||||
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isPassableByReference(Node $arg): bool
|
||||
{
|
||||
if (!\property_exists($arg, 'value')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unpacked arrays can be passed by reference
|
||||
if ($arg->value instanceof Array_) {
|
||||
return \property_exists($arg, 'unpack') && $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 ||
|
||||
$arg->value instanceof PropertyFetch ||
|
||||
$arg->value instanceof Variable ||
|
||||
$arg->value instanceof FuncCall ||
|
||||
$arg->value instanceof MethodCall ||
|
||||
$arg->value instanceof StaticCall ||
|
||||
$arg->value instanceof ArrayDimFetch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Because array_multisort has a problematic signature...
|
||||
*
|
||||
* The argument order is all sorts of wonky, and whether something is passed
|
||||
* by reference or not depends on the values of the two arguments before it.
|
||||
* We'll do a good faith attempt at validating this, but err on the side of
|
||||
* permissive.
|
||||
*
|
||||
* This is why you don't design languages where core code and extensions can
|
||||
* implement APIs that wouldn't be possible in userland code.
|
||||
*
|
||||
* @throws FatalErrorException for clearly invalid arguments
|
||||
*
|
||||
* @param Node $node
|
||||
*/
|
||||
private function validateArrayMultisort(Node $node)
|
||||
{
|
||||
$nonPassable = 2; // start with 2 because the first one has to be passable by reference
|
||||
foreach ($node->args as $arg) {
|
||||
if ($this->isPassableByReference($arg)) {
|
||||
$nonPassable = 0;
|
||||
} elseif (++$nonPassable > 2) {
|
||||
// There can be *at most* two non-passable-by-reference args in a row. This is about
|
||||
// as close as we can get to validating the arguments for this function :-/
|
||||
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
138
vendor/psy/psysh/src/CodeCleaner/RequirePass.php
vendored
Normal file
138
vendor/psy/psysh/src/CodeCleaner/RequirePass.php
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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 const REQUIRE_TYPES = [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 null;
|
||||
}
|
||||
|
||||
$node = clone $origNode;
|
||||
|
||||
/*
|
||||
* rewrite
|
||||
*
|
||||
* $foo = require $bar
|
||||
*
|
||||
* to
|
||||
*
|
||||
* $foo = require \Psy\CodeCleaner\RequirePass::resolve($bar)
|
||||
*/
|
||||
// PHP-Parser 4.x uses LNumber, 5.x has LNumber as an alias to Int_
|
||||
// Just use LNumber for compatibility with both versions
|
||||
// @todo Switch to Int_ once we drop support for PHP-Parser 4.x
|
||||
$arg = new LNumber($origNode->getStartLine());
|
||||
|
||||
$node->expr = new StaticCall(
|
||||
new FullyQualifiedName(self::class),
|
||||
'resolve',
|
||||
[new Arg($origNode->expr), new Arg($arg)],
|
||||
$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 $startLine 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, $startLine = 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, $startLine);
|
||||
}
|
||||
// @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, $startLine);
|
||||
}
|
||||
|
||||
// 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::REQUIRE_TYPES);
|
||||
}
|
||||
|
||||
private static function getIncludePath(): array
|
||||
{
|
||||
if (\PATH_SEPARATOR === ':') {
|
||||
return \preg_split('#:(?!//)#', \get_include_path());
|
||||
}
|
||||
|
||||
return \explode(\PATH_SEPARATOR, \get_include_path());
|
||||
}
|
||||
}
|
||||
123
vendor/psy/psysh/src/CodeCleaner/ReturnTypePass.php
vendored
Normal file
123
vendor/psy/psysh/src/CodeCleaner/ReturnTypePass.php
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\IntersectionType;
|
||||
use PhpParser\Node\Name;
|
||||
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 array $returnTypeStack = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($this->isFunctionNode($node)) {
|
||||
$this->returnTypeStack[] = \property_exists($node, 'returnType') ? $node->returnType : null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!empty($this->returnTypeStack) && $node instanceof Return_) {
|
||||
$expectedType = \end($this->returnTypeStack);
|
||||
if ($expectedType === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$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->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int|Node|Node[]|null Replacement node (or special return value)
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if (!empty($this->returnTypeStack) && $this->isFunctionNode($node)) {
|
||||
\array_pop($this->returnTypeStack);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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 IntersectionType) {
|
||||
return \implode('&', \array_map([$this, 'typeName'], $node->types));
|
||||
}
|
||||
|
||||
if ($node instanceof NullableType) {
|
||||
return $this->typeName($node->type);
|
||||
}
|
||||
|
||||
if ($node instanceof Identifier || $node instanceof Name) {
|
||||
return $node->toLowerString();
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Unable to find type name');
|
||||
}
|
||||
}
|
||||
94
vendor/psy/psysh/src/CodeCleaner/StrictTypesPass.php
vendored
Normal file
94
vendor/psy/psysh/src/CodeCleaner/StrictTypesPass.php
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\DeclareItem;
|
||||
use PhpParser\Node\Scalar\Int_;
|
||||
use PhpParser\Node\Scalar\LNumber;
|
||||
use PhpParser\Node\Stmt\Declare_;
|
||||
use PhpParser\Node\Stmt\DeclareDeclare;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Provide implicit strict types declarations for for subsequent execution.
|
||||
*
|
||||
* The strict types pass remembers the last strict types declaration:
|
||||
*
|
||||
* declare(strict_types=1);
|
||||
*
|
||||
* ... which it then applies implicitly to all future evaluated code, until it
|
||||
* is replaced by a new declaration.
|
||||
*/
|
||||
class StrictTypesPass extends CodeCleanerPass
|
||||
{
|
||||
const EXCEPTION_MESSAGE = 'strict_types declaration must have 0 or 1 as its value';
|
||||
|
||||
private bool $strictTypes;
|
||||
|
||||
/**
|
||||
* @param bool $strictTypes enforce strict types by default
|
||||
*/
|
||||
public function __construct(bool $strictTypes = false)
|
||||
{
|
||||
$this->strictTypes = $strictTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is a standalone strict types declaration, remember it for later.
|
||||
*
|
||||
* Otherwise, apply remembered strict types declaration to to the code until
|
||||
* a new declaration is encountered.
|
||||
*
|
||||
* @throws FatalErrorException if an invalid `strict_types` declaration is found
|
||||
*
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$prependStrictTypes = $this->strictTypes;
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
if ($node instanceof Declare_) {
|
||||
foreach ($node->declares as $declare) {
|
||||
if ($declare->key->toString() === 'strict_types') {
|
||||
$value = $declare->value;
|
||||
// @todo Remove LNumber once we drop support for PHP-Parser 4.x
|
||||
if ((!$value instanceof LNumber && !$value instanceof Int_) || ($value->value !== 0 && $value->value !== 1)) {
|
||||
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
|
||||
$this->strictTypes = $value->value === 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($prependStrictTypes) {
|
||||
$first = \reset($nodes);
|
||||
if (!$first instanceof Declare_) {
|
||||
// @todo Switch to PhpParser\Node\DeclareItem once we drop support for PHP-Parser 4.x
|
||||
// @todo Remove LNumber once we drop support for PHP-Parser 4.x
|
||||
$arg = \class_exists('PhpParser\Node\Scalar\Int_') ? new Int_(1) : new LNumber(1);
|
||||
$declareItem = \class_exists('PhpParser\Node\DeclareItem') ?
|
||||
new DeclareItem('strict_types', $arg) :
|
||||
new DeclareDeclare('strict_types', $arg);
|
||||
$declare = new Declare_([$declareItem]);
|
||||
\array_unshift($nodes, $declare);
|
||||
}
|
||||
}
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
}
|
||||
162
vendor/psy/psysh/src/CodeCleaner/UseStatementPass.php
vendored
Normal file
162
vendor/psy/psysh/src/CodeCleaner/UseStatementPass.php
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\Namespace_;
|
||||
use PhpParser\Node\Stmt\Use_;
|
||||
use PhpParser\Node\Stmt\UseItem;
|
||||
use PhpParser\Node\Stmt\UseUse;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Provide implicit use statements for subsequent execution.
|
||||
*
|
||||
* The use statement pass remembers the last use statement line encountered:
|
||||
*
|
||||
* use Foo\Bar as Baz;
|
||||
*
|
||||
* ... which it then applies implicitly to all future evaluated code, until the
|
||||
* current namespace is replaced by another namespace.
|
||||
*
|
||||
* Extends NamespaceAwarePass to leverage shared alias tracking.
|
||||
*/
|
||||
class UseStatementPass extends NamespaceAwarePass
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
// Check for use statement conflicts BEFORE parent adds it to aliases
|
||||
// Skip re-injected use statements (marked with 'psyshReinjected' attribute)
|
||||
if ($node instanceof Use_ && !$node->getAttribute('psyshReinjected')) {
|
||||
$this->validateUseStatement($node);
|
||||
}
|
||||
|
||||
return parent::enterNode($node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-inject use statements from previous inputs.
|
||||
*
|
||||
* Each REPL input is evaluated separately; re-injecting use statements matches PHP behavior for
|
||||
* namespaces and use statements in a file.
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
parent::beforeTraverse($nodes);
|
||||
|
||||
if (!$this->cleaner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for namespace declarations in the input
|
||||
foreach ($nodes as $node) {
|
||||
if ($node instanceof Namespace_) {
|
||||
// Only re-inject use statements if this is a wrapper created by NamespacePass.
|
||||
// This matches PHP behavior: explicit namespace declaration clears use statements.
|
||||
if ($node->getAttribute('psyshReinjected')) {
|
||||
$aliasesByType = $this->cleaner->getAliasesByTypeForNamespace($node->name);
|
||||
if (!empty($aliasesByType)) {
|
||||
$useStatements = $this->createUseStatements($aliasesByType);
|
||||
$node->stmts = \array_merge($useStatements, $node->stmts ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't process other nodes or return modified nodes
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// No namespace declaration in input, or re-applied by NamespacePass; re-inject use
|
||||
// statements for the empty namespace.
|
||||
$aliasesByType = $this->cleaner->getAliasesByTypeForNamespace(null);
|
||||
if (!empty($aliasesByType)) {
|
||||
$useStatements = $this->createUseStatements($aliasesByType);
|
||||
$nodes = \array_merge($useStatements, $nodes);
|
||||
}
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have aliases but didn't leave a namespace (global namespace case), persist them to
|
||||
* CodeCleaner for the next traversal.
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function afterTraverse(array $nodes)
|
||||
{
|
||||
if (!$this->cleaner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Persist aliases if they're at the global level (not inside any namespace)
|
||||
if (!empty($this->aliasesByType)) {
|
||||
$this->cleaner->setAliasesByTypeForNamespace(null, $this->aliasesByType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a use statement doesn't conflict with existing aliases.
|
||||
*
|
||||
* @throws FatalErrorException if the alias is already in use
|
||||
*
|
||||
* @param Use_ $stmt The use statement node
|
||||
*/
|
||||
private function validateUseStatement(Use_ $stmt): void
|
||||
{
|
||||
$seenAliases = [];
|
||||
|
||||
foreach ($stmt->uses as $useItem) {
|
||||
$alias = \strtolower($useItem->getAlias());
|
||||
$type = $this->getUseImportType($stmt, $useItem);
|
||||
|
||||
if (isset($seenAliases[$type][$alias]) || isset($this->getAliasesForType($type)[$alias])) {
|
||||
throw new FatalErrorException(\sprintf('Cannot use %s as %s because the name is already in use', $useItem->name->toString(), $useItem->getAlias()), 0, \E_ERROR, null, $stmt->getStartLine());
|
||||
}
|
||||
|
||||
$seenAliases[$type][$alias] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create use statement nodes from stored aliases.
|
||||
*
|
||||
* @param array $aliasesByType Map of Use_::TYPE_* constants to alias maps
|
||||
*
|
||||
* @return Use_[] Array of use statement nodes
|
||||
*/
|
||||
private function createUseStatements(array $aliasesByType): array
|
||||
{
|
||||
$useStatements = [];
|
||||
|
||||
foreach ([Use_::TYPE_NORMAL, Use_::TYPE_FUNCTION, Use_::TYPE_CONSTANT] as $type) {
|
||||
foreach ($aliasesByType[$type] ?? [] as $alias => $name) {
|
||||
// Create UseItem (PHP-Parser 5.x) or UseUse (PHP-Parser 4.x)
|
||||
$useItem = \class_exists(UseItem::class)
|
||||
? new UseItem($name, new Identifier($alias))
|
||||
: new UseUse($name, $alias);
|
||||
// Mark as re-injected so we don't validate it
|
||||
$useStatements[] = new Use_([$useItem], $type, ['psyshReinjected' => true]);
|
||||
}
|
||||
}
|
||||
|
||||
return $useStatements;
|
||||
}
|
||||
}
|
||||
332
vendor/psy/psysh/src/CodeCleaner/ValidClassNamePass.php
vendored
Normal file
332
vendor/psy/psysh/src/CodeCleaner/ValidClassNamePass.php
vendored
Normal file
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\Ternary;
|
||||
use PhpParser\Node\Stmt;
|
||||
use PhpParser\Node\Stmt\Class_;
|
||||
use PhpParser\Node\Stmt\Do_;
|
||||
use PhpParser\Node\Stmt\If_;
|
||||
use PhpParser\Node\Stmt\Interface_;
|
||||
use PhpParser\Node\Stmt\Switch_;
|
||||
use PhpParser\Node\Stmt\Trait_;
|
||||
use PhpParser\Node\Stmt\While_;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Validate that classes exist.
|
||||
*
|
||||
* This pass throws a FatalErrorException rather than letting PHP run
|
||||
* headfirst into a real fatal error and die.
|
||||
*/
|
||||
class ValidClassNamePass extends NamespaceAwarePass
|
||||
{
|
||||
const CLASS_TYPE = 'class';
|
||||
const INTERFACE_TYPE = 'interface';
|
||||
const TRAIT_TYPE = 'trait';
|
||||
|
||||
private int $conditionalScopes = 0;
|
||||
|
||||
/**
|
||||
* Validate class, interface and trait definitions.
|
||||
*
|
||||
* Validate them upon entering the node, so that we know about their
|
||||
* presence and can validate constant fetches and static calls in class or
|
||||
* trait methods.
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
parent::enterNode($node);
|
||||
|
||||
if (self::isConditional($node)) {
|
||||
$this->conditionalScopes++;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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 null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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--;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function isConditional(Node $node): bool
|
||||
{
|
||||
return $node instanceof If_ ||
|
||||
$node instanceof While_ ||
|
||||
$node instanceof Do_ ||
|
||||
$node instanceof Switch_ ||
|
||||
$node instanceof Ternary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a class definition statement.
|
||||
*
|
||||
* @param Class_ $stmt
|
||||
*/
|
||||
protected function validateClassStatement(Class_ $stmt)
|
||||
{
|
||||
$this->ensureCanDefine($stmt, self::CLASS_TYPE);
|
||||
if (isset($stmt->extends)) {
|
||||
$this->ensureClassExists($this->getFullyQualifiedName($stmt->extends), $stmt);
|
||||
}
|
||||
$this->ensureInterfacesExist($stmt->implements, $stmt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an interface definition statement.
|
||||
*
|
||||
* @param Interface_ $stmt
|
||||
*/
|
||||
protected function validateInterfaceStatement(Interface_ $stmt)
|
||||
{
|
||||
$this->ensureCanDefine($stmt, self::INTERFACE_TYPE);
|
||||
$this->ensureInterfacesExist($stmt->extends, $stmt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a trait definition statement.
|
||||
*
|
||||
* @param Trait_ $stmt
|
||||
*/
|
||||
protected function validateTraitStatement(Trait_ $stmt)
|
||||
{
|
||||
$this->ensureCanDefine($stmt, self::TRAIT_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that no class, interface or trait name collides with a new definition.
|
||||
*
|
||||
* @throws FatalErrorException
|
||||
*
|
||||
* @param Stmt $stmt
|
||||
* @param string $scopeType
|
||||
*/
|
||||
protected function ensureCanDefine(Stmt $stmt, string $scopeType = self::CLASS_TYPE)
|
||||
{
|
||||
// Anonymous classes don't have a name, and uniqueness shouldn't be enforced.
|
||||
if (!\property_exists($stmt, 'name') || $stmt->name === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$name = $this->getFullyQualifiedName($stmt->name);
|
||||
|
||||
// check for name collisions
|
||||
$errorType = null;
|
||||
if ($this->classExists($name)) {
|
||||
$errorType = self::CLASS_TYPE;
|
||||
} elseif ($this->interfaceExists($name)) {
|
||||
$errorType = self::INTERFACE_TYPE;
|
||||
} elseif ($this->traitExists($name)) {
|
||||
$errorType = self::TRAIT_TYPE;
|
||||
}
|
||||
|
||||
if ($errorType !== null) {
|
||||
throw $this->createError(\sprintf('%s named %s already exists', \ucfirst($errorType), $name), $stmt);
|
||||
}
|
||||
|
||||
// Store creation for the rest of this code snippet so we can find local
|
||||
// issue too
|
||||
$this->currentScope[\strtolower($name)] = $scopeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a referenced class exists.
|
||||
*
|
||||
* @throws FatalErrorException
|
||||
*
|
||||
* @param string $name
|
||||
* @param Stmt $stmt
|
||||
*/
|
||||
protected function ensureClassExists(string $name, Stmt $stmt)
|
||||
{
|
||||
if (!$this->classExists($name)) {
|
||||
throw $this->createError(\sprintf('Class \'%s\' not found', $name), $stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a referenced class _or interface_ exists.
|
||||
*
|
||||
* @throws FatalErrorException
|
||||
*
|
||||
* @param string $name
|
||||
* @param Stmt $stmt
|
||||
*/
|
||||
protected function ensureClassOrInterfaceExists(string $name, Stmt $stmt)
|
||||
{
|
||||
if (!$this->classExists($name) && !$this->interfaceExists($name)) {
|
||||
throw $this->createError(\sprintf('Class \'%s\' not found', $name), $stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a referenced class _or trait_ exists.
|
||||
*
|
||||
* @throws FatalErrorException
|
||||
*
|
||||
* @param string $name
|
||||
* @param Stmt $stmt
|
||||
*/
|
||||
protected function ensureClassOrTraitExists(string $name, Stmt $stmt)
|
||||
{
|
||||
if (!$this->classExists($name) && !$this->traitExists($name)) {
|
||||
throw $this->createError(\sprintf('Class \'%s\' not found', $name), $stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a statically called method exists.
|
||||
*
|
||||
* @throws FatalErrorException
|
||||
*
|
||||
* @param string $class
|
||||
* @param string $name
|
||||
* @param Stmt $stmt
|
||||
*/
|
||||
protected function ensureMethodExists(string $class, string $name, Stmt $stmt)
|
||||
{
|
||||
$this->ensureClassOrTraitExists($class, $stmt);
|
||||
|
||||
// let's pretend all calls to self, parent and static are valid
|
||||
if (\in_array(\strtolower($class), ['self', 'parent', 'static'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ... and all calls to classes defined right now
|
||||
if ($this->findInScope($class) === self::CLASS_TYPE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if method name is an expression, give it a pass for now
|
||||
if ($name instanceof Expr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\method_exists($class, $name) && !\method_exists($class, '__callStatic')) {
|
||||
throw $this->createError(\sprintf('Call to undefined method %s::%s()', $class, $name), $stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a referenced interface exists.
|
||||
*
|
||||
* @throws FatalErrorException
|
||||
*
|
||||
* @param Interface_[] $interfaces
|
||||
* @param Stmt $stmt
|
||||
*/
|
||||
protected function ensureInterfacesExist(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a class exists, or has been defined in the current code snippet.
|
||||
*
|
||||
* Gives `self`, `static` and `parent` a free pass.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
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), ['self', 'static', 'parent'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return \class_exists($name) || $this->findInScope($name) === self::CLASS_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an interface exists, or has been defined in the current code snippet.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
protected function interfaceExists(string $name): bool
|
||||
{
|
||||
return \interface_exists($name) || $this->findInScope($name) === self::INTERFACE_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a trait exists, or has been defined in the current code snippet.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
protected function traitExists(string $name): bool
|
||||
{
|
||||
return \trait_exists($name) || $this->findInScope($name) === self::TRAIT_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a symbol in the current code snippet scope.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function findInScope(string $name)
|
||||
{
|
||||
$name = \strtolower($name);
|
||||
if (isset($this->currentScope[$name])) {
|
||||
return $this->currentScope[$name];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error creation factory.
|
||||
*
|
||||
* @param string $msg
|
||||
* @param Stmt $stmt
|
||||
*/
|
||||
protected function createError(string $msg, Stmt $stmt): FatalErrorException
|
||||
{
|
||||
return new FatalErrorException($msg, 0, \E_ERROR, null, $stmt->getStartLine());
|
||||
}
|
||||
}
|
||||
125
vendor/psy/psysh/src/CodeCleaner/ValidConstructorPass.php
vendored
Normal file
125
vendor/psy/psysh/src/CodeCleaner/ValidConstructorPass.php
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\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 array $namespace = [];
|
||||
|
||||
/**
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->namespace = [];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) ? $this->getParts($node->name) : [];
|
||||
} 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 (\property_exists($stmt, 'name') && \strtolower($stmt->name) === '__construct') {
|
||||
$this->validateConstructor($stmt, $node);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// We found a possible old-style constructor (unless there is also a __construct method)
|
||||
if (empty($this->namespace) && $node->name !== null && \property_exists($stmt, 'name') && \strtolower($node->name) === \strtolower($stmt->name)) {
|
||||
$constructor = $stmt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($constructor) {
|
||||
$this->validateConstructor($constructor, $node);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 (\method_exists($constructor, 'isStatic') && $constructor->isStatic()) {
|
||||
$msg = \sprintf(
|
||||
'Constructor %s::%s() cannot be static',
|
||||
\implode('\\', \array_merge($this->namespace, (array) $classNode->name->toString())),
|
||||
\property_exists($constructor, 'name') ? $constructor->name : '__construct'
|
||||
);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $classNode->getStartLine());
|
||||
}
|
||||
|
||||
if (\method_exists($constructor, 'getReturnType') && $constructor->getReturnType()) {
|
||||
$msg = \sprintf(
|
||||
'Constructor %s::%s() cannot declare a return type',
|
||||
\implode('\\', \array_merge($this->namespace, (array) $classNode->name->toString())),
|
||||
\property_exists($constructor, 'name') ? $constructor->name : '__construct'
|
||||
);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $classNode->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backwards compatibility shim for PHP-Parser 4.x.
|
||||
*
|
||||
* At some point we might want to make $namespace a plain string, to match how Name works?
|
||||
*/
|
||||
protected function getParts(Name $name): array
|
||||
{
|
||||
return \method_exists($name, 'getParts') ? $name->getParts() : $name->parts;
|
||||
}
|
||||
}
|
||||
87
vendor/psy/psysh/src/CodeCleaner/ValidFunctionNamePass.php
vendored
Normal file
87
vendor/psy/psysh/src/CodeCleaner/ValidFunctionNamePass.php
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt\Do_;
|
||||
use PhpParser\Node\Stmt\Function_;
|
||||
use PhpParser\Node\Stmt\If_;
|
||||
use PhpParser\Node\Stmt\Switch_;
|
||||
use PhpParser\Node\Stmt\While_;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Validate that function calls will succeed.
|
||||
*
|
||||
* This pass throws a FatalErrorException rather than letting PHP run
|
||||
* headfirst into a real fatal error and die.
|
||||
*/
|
||||
class ValidFunctionNamePass extends NamespaceAwarePass
|
||||
{
|
||||
private int $conditionalScopes = 0;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
parent::enterNode($node);
|
||||
|
||||
if (self::isConditional($node)) {
|
||||
$this->conditionalScopes++;
|
||||
} elseif ($node instanceof Function_) {
|
||||
$name = $this->getFullyQualifiedName($node->name);
|
||||
|
||||
// @todo add an "else" here which adds a runtime check for instances where we can't tell
|
||||
// whether a function is being redefined by static analysis alone.
|
||||
if ($this->conditionalScopes === 0) {
|
||||
if (\function_exists($name) ||
|
||||
isset($this->currentScope[\strtolower($name)])) {
|
||||
$msg = \sprintf('Cannot redeclare %s()', $name);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
$this->currentScope[\strtolower($name)] = true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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--;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function isConditional(Node $node)
|
||||
{
|
||||
return $node instanceof If_ ||
|
||||
$node instanceof While_ ||
|
||||
$node instanceof Do_ ||
|
||||
$node instanceof Switch_;
|
||||
}
|
||||
}
|
||||
26
vendor/psy/psysh/src/CodeCleanerAware.php
vendored
Normal file
26
vendor/psy/psysh/src/CodeCleanerAware.php
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy;
|
||||
|
||||
/**
|
||||
* CodeCleanerAware interface.
|
||||
*
|
||||
* This interface is used to pass the Shell's CodeCleaner into commands which
|
||||
* require access to name resolution via use statements and namespace context.
|
||||
*/
|
||||
interface CodeCleanerAware
|
||||
{
|
||||
/**
|
||||
* Set the CodeCleaner instance.
|
||||
*/
|
||||
public function setCodeCleaner(CodeCleaner $cleaner);
|
||||
}
|
||||
112
vendor/psy/psysh/src/Command/BufferCommand.php
vendored
Normal file
112
vendor/psy/psysh/src/Command/BufferCommand.php
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use Psy\Output\ShellOutputAdapter;
|
||||
use Psy\Readline\LegacyReadline;
|
||||
use Psy\Readline\Readline;
|
||||
use Psy\Readline\ReadlineAware;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Interact with the current code buffer.
|
||||
*
|
||||
* Shows and clears the buffer for the current multi-line expression.
|
||||
*/
|
||||
class BufferCommand extends Command implements ReadlineAware
|
||||
{
|
||||
private ?Readline $readline = null;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('buffer')
|
||||
->setAliases(['buf'])
|
||||
->setDefinition([
|
||||
new InputOption('clear', '', InputOption::VALUE_NONE, 'Clear the current buffer.'),
|
||||
])
|
||||
->setDescription('Show (or clear) the contents of the code input buffer.')
|
||||
->setHelp(
|
||||
<<<'HELP'
|
||||
Show the contents of the code buffer for the current multi-line expression.
|
||||
|
||||
Optionally, clear the buffer by passing the <info>--clear</info> option.
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int 0 if everything went fine, or an exit code
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$shell = $this->getShell();
|
||||
$shellOutput = $this->shellOutput($output);
|
||||
$readline = $this->getLegacyReadline();
|
||||
$legacyBuffer = $readline->getBuffer();
|
||||
$shellBuffer = $shell->getPendingCodeBuffer();
|
||||
$buf = $legacyBuffer !== [] ? $legacyBuffer : $shellBuffer;
|
||||
if ($input->getOption('clear')) {
|
||||
$readline->clearBuffer();
|
||||
if ($shellBuffer !== []) {
|
||||
$shell->clearPendingCodeBuffer();
|
||||
}
|
||||
$shellOutput->writeln($this->formatLines($buf, 'urgent'), ShellOutputAdapter::NUMBER_LINES);
|
||||
} else {
|
||||
$shellOutput->writeln($this->formatLines($buf), ShellOutputAdapter::NUMBER_LINES);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the shell's readline implementation.
|
||||
*/
|
||||
public function setReadline(Readline $readline)
|
||||
{
|
||||
$this->readline = $readline;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method for wrapping buffer lines in `<urgent>` and `<return>` formatter strings.
|
||||
*
|
||||
* @param array $lines
|
||||
* @param string $type (default: 'return')
|
||||
*
|
||||
* @return array Formatted strings
|
||||
*/
|
||||
protected function formatLines(array $lines, string $type = 'return'): array
|
||||
{
|
||||
$template = \sprintf('<%s>%%s</%s>', $type, $type);
|
||||
|
||||
return \array_map(fn ($line) => \sprintf($template, $line), $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active multiline buffer from the legacy shim.
|
||||
*/
|
||||
private function getLegacyReadline(): LegacyReadline
|
||||
{
|
||||
if ($this->readline instanceof LegacyReadline) {
|
||||
return $this->readline;
|
||||
}
|
||||
|
||||
throw new \LogicException('BufferCommand requires LegacyReadline.');
|
||||
}
|
||||
}
|
||||
53
vendor/psy/psysh/src/Command/ClearCommand.php
vendored
Normal file
53
vendor/psy/psysh/src/Command/ClearCommand.php
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Clear the Psy Shell.
|
||||
*
|
||||
* Just what it says on the tin.
|
||||
*/
|
||||
class ClearCommand extends Command
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('clear')
|
||||
->setDefinition([])
|
||||
->setDescription('Clear the Psy Shell screen.')
|
||||
->setHelp(
|
||||
<<<'HELP'
|
||||
Clear the Psy Shell screen.
|
||||
|
||||
Pro Tip: If your PHP has readline support, you should be able to use ctrl+l too!
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int 0 if everything went fine, or an exit code
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$output->write(\sprintf('%c[2J%c[0;0f', 27, 27));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
60
vendor/psy/psysh/src/Command/CodeArgumentParser.php
vendored
Normal file
60
vendor/psy/psysh/src/Command/CodeArgumentParser.php
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\Error as PhpParserError;
|
||||
use PhpParser\Parser;
|
||||
use Psy\Exception\ParseErrorException;
|
||||
use Psy\ParserFactory;
|
||||
|
||||
/**
|
||||
* Class CodeArgumentParser.
|
||||
*/
|
||||
class CodeArgumentParser
|
||||
{
|
||||
private Parser $parser;
|
||||
|
||||
public function __construct(?Parser $parser = null)
|
||||
{
|
||||
$this->parser = $parser ?? (new ParserFactory())->createParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lex and parse a string of code into statements.
|
||||
*
|
||||
* This is intended for code arguments, so the code string *should not* start with <?php
|
||||
*
|
||||
* @throws ParseErrorException
|
||||
*
|
||||
* @return array Statements
|
||||
*/
|
||||
public function parse(string $code): array
|
||||
{
|
||||
$code = '<?php '.$code;
|
||||
|
||||
try {
|
||||
return $this->parser->parse($code);
|
||||
} catch (PhpParserError $e) {
|
||||
if (\strpos($e->getMessage(), 'unexpected EOF') === false) {
|
||||
throw ParseErrorException::fromParseError($e);
|
||||
}
|
||||
|
||||
// If we got an unexpected EOF, let's try it again with a semicolon.
|
||||
try {
|
||||
return $this->parser->parse($code.';');
|
||||
} catch (PhpParserError $_e) {
|
||||
// Throw the original error, not the semicolon one.
|
||||
throw ParseErrorException::fromParseError($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
315
vendor/psy/psysh/src/Command/Command.php
vendored
Normal file
315
vendor/psy/psysh/src/Command/Command.php
vendored
Normal file
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\CodeCleanerAware;
|
||||
use Psy\ContextAware;
|
||||
use Psy\Output\ShellOutputAdapter;
|
||||
use Psy\Readline\ReadlineAware;
|
||||
use Psy\Shell;
|
||||
use Psy\VarDumper\PresenterAware;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command as BaseCommand;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Helper\TableStyle;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* The Psy Shell base command.
|
||||
*/
|
||||
abstract class Command extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* Sets the application instance for this command.
|
||||
*
|
||||
* @param Application|null $application An Application instance
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function setApplication(?Application $application = null): void
|
||||
{
|
||||
if ($application !== null && !$application instanceof Shell) {
|
||||
throw new \InvalidArgumentException('PsySH Commands require an instance of Psy\Shell');
|
||||
}
|
||||
|
||||
parent::setApplication($application);
|
||||
}
|
||||
|
||||
/**
|
||||
* getApplication, but is guaranteed to return a Shell instance.
|
||||
*/
|
||||
protected function getShell(): Shell
|
||||
{
|
||||
$shell = $this->getApplication();
|
||||
if (!$shell instanceof Shell) {
|
||||
throw new \RuntimeException('PsySH Commands require an instance of Psy\Shell');
|
||||
}
|
||||
|
||||
return $shell;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function run(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
if (
|
||||
$this instanceof ContextAware ||
|
||||
$this instanceof CodeCleanerAware ||
|
||||
$this instanceof PresenterAware ||
|
||||
$this instanceof ReadlineAware
|
||||
) {
|
||||
$this->getShell()->boot($input, $output);
|
||||
}
|
||||
|
||||
return parent::run($input, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function asText(): string
|
||||
{
|
||||
$messages = [
|
||||
'<comment>Usage:</comment>',
|
||||
' '.$this->getSynopsis(),
|
||||
'',
|
||||
];
|
||||
|
||||
if ($this->getAliases()) {
|
||||
$messages[] = $this->aliasesAsText();
|
||||
}
|
||||
|
||||
if ($this->getArguments()) {
|
||||
$messages[] = $this->argumentsAsText();
|
||||
}
|
||||
|
||||
if ($this->getOptions()) {
|
||||
$messages[] = $this->optionsAsText();
|
||||
}
|
||||
|
||||
if ($help = $this->getProcessedHelp()) {
|
||||
$messages[] = '<comment>Help:</comment>';
|
||||
$messages[] = ' '.\str_replace("\n", "\n ", $help)."\n";
|
||||
}
|
||||
|
||||
return \implode("\n", $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render help text for the current input context.
|
||||
*/
|
||||
public function asTextForInput(InputInterface $input): string
|
||||
{
|
||||
return $this->asText();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
private function getArguments(): array
|
||||
{
|
||||
$hidden = $this->getHiddenArguments();
|
||||
|
||||
return \array_filter(
|
||||
$this->getNativeDefinition()->getArguments(),
|
||||
fn ($argument) => !\in_array($argument->getName(), $hidden)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* These arguments will be excluded from help output.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getHiddenArguments(): array
|
||||
{
|
||||
return ['command'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
private function getOptions(): array
|
||||
{
|
||||
$hidden = $this->getHiddenOptions();
|
||||
|
||||
return \array_filter(
|
||||
$this->getNativeDefinition()->getOptions(),
|
||||
fn ($option) => !\in_array($option->getName(), $hidden)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* These options will be excluded from help output.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getHiddenOptions(): array
|
||||
{
|
||||
return ['verbose'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format command aliases as text..
|
||||
*/
|
||||
private function aliasesAsText(): string
|
||||
{
|
||||
return '<comment>Aliases:</comment> <info>'.\implode(', ', $this->getAliases()).'</info>'.\PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format command arguments as text.
|
||||
*/
|
||||
private function argumentsAsText(): string
|
||||
{
|
||||
$max = $this->getMaxWidth();
|
||||
$messages = [];
|
||||
|
||||
$arguments = $this->getArguments();
|
||||
if (!empty($arguments)) {
|
||||
$messages[] = '<comment>Arguments:</comment>';
|
||||
foreach ($arguments as $argument) {
|
||||
if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) {
|
||||
$default = \sprintf('<comment> (default: %s)</comment>', $this->formatDefaultValue($argument->getDefault()));
|
||||
} else {
|
||||
$default = '';
|
||||
}
|
||||
|
||||
$name = $argument->getName();
|
||||
// @phan-suppress-next-line PhanParamSuspiciousOrder - intentionally padding empty string to create spaces
|
||||
$pad = \str_pad('', $max - \strlen($name));
|
||||
// @phan-suppress-next-line PhanParamSuspiciousOrder - intentionally padding empty string to create spaces
|
||||
$description = \str_replace("\n", "\n".\str_pad('', $max + 2, ' '), $argument->getDescription());
|
||||
|
||||
$messages[] = \sprintf(' <info>%s</info>%s %s%s', $name, $pad, $description, $default);
|
||||
}
|
||||
|
||||
$messages[] = '';
|
||||
}
|
||||
|
||||
return \implode(\PHP_EOL, $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format options as text.
|
||||
*/
|
||||
private function optionsAsText(): string
|
||||
{
|
||||
$max = $this->getMaxWidth();
|
||||
$messages = [];
|
||||
|
||||
$options = $this->getOptions();
|
||||
if ($options) {
|
||||
$messages[] = '<comment>Options:</comment>';
|
||||
|
||||
foreach ($options as $option) {
|
||||
if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) {
|
||||
$default = \sprintf('<comment> (default: %s)</comment>', $this->formatDefaultValue($option->getDefault()));
|
||||
} else {
|
||||
$default = '';
|
||||
}
|
||||
|
||||
$multiple = $option->isArray() ? '<comment> (multiple values allowed)</comment>' : '';
|
||||
// @phan-suppress-next-line PhanParamSuspiciousOrder - intentionally padding empty string to create spaces
|
||||
$description = \str_replace("\n", "\n".\str_pad('', $max + 2, ' '), $option->getDescription());
|
||||
|
||||
$optionMax = $max - \strlen($option->getName()) - 2;
|
||||
$messages[] = \sprintf(
|
||||
" <info>%s</info> %-{$optionMax}s%s%s%s",
|
||||
'--'.$option->getName(),
|
||||
$option->getShortcut() ? \sprintf('(-%s) ', $option->getShortcut()) : '',
|
||||
$description,
|
||||
$default,
|
||||
$multiple
|
||||
);
|
||||
}
|
||||
|
||||
$messages[] = '';
|
||||
}
|
||||
|
||||
return \implode(\PHP_EOL, $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the maximum padding width for a set of lines.
|
||||
*/
|
||||
private function getMaxWidth(): int
|
||||
{
|
||||
$max = 0;
|
||||
|
||||
foreach ($this->getOptions() as $option) {
|
||||
$nameLength = \strlen($option->getName()) + 2;
|
||||
if ($option->getShortcut()) {
|
||||
$nameLength += \strlen($option->getShortcut()) + 3;
|
||||
}
|
||||
|
||||
$max = \max($max, $nameLength);
|
||||
}
|
||||
|
||||
foreach ($this->getArguments() as $argument) {
|
||||
$max = \max($max, \strlen($argument->getName()));
|
||||
}
|
||||
|
||||
return ++$max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an option default as text.
|
||||
*
|
||||
* @param mixed $default
|
||||
*/
|
||||
private function formatDefaultValue($default): string
|
||||
{
|
||||
if (\is_array($default) && $default === \array_values($default)) {
|
||||
return \sprintf("['%s']", \implode("', '", $default));
|
||||
}
|
||||
|
||||
return \str_replace("\n", '', \var_export($default, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Table instance.
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
protected function getTable(OutputInterface $output)
|
||||
{
|
||||
$style = new TableStyle();
|
||||
|
||||
// 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([])
|
||||
->setStyle($style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a ShellOutputAdapter for the given output.
|
||||
*/
|
||||
protected function shellOutput(OutputInterface $output): ShellOutputAdapter
|
||||
{
|
||||
return new ShellOutputAdapter($output);
|
||||
}
|
||||
}
|
||||
562
vendor/psy/psysh/src/Command/Config/AbstractConfigCommand.php
vendored
Normal file
562
vendor/psy/psysh/src/Command/Config/AbstractConfigCommand.php
vendored
Normal file
@@ -0,0 +1,562 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command\Config;
|
||||
|
||||
use Psy\Command\Command;
|
||||
use Psy\Configuration;
|
||||
use Psy\Output\Theme;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
|
||||
/**
|
||||
* Base class for runtime configuration subcommands.
|
||||
*/
|
||||
abstract class AbstractConfigCommand extends Command
|
||||
{
|
||||
private ?Configuration $config = null;
|
||||
private ?array $options = null;
|
||||
|
||||
public function setConfiguration(Configuration $config): void
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->options = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array Associative array of option definitions keyed by lowercase name
|
||||
*/
|
||||
protected function getOptions(): array
|
||||
{
|
||||
if ($this->options !== null) {
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
$config = $this->getConfig();
|
||||
$booleanParser = function (string $name, string $acceptedValues): callable {
|
||||
return function (string $value) use ($name, $acceptedValues): bool {
|
||||
switch (\strtolower($value)) {
|
||||
case '1':
|
||||
case 'true':
|
||||
case 'yes':
|
||||
case 'on':
|
||||
return true;
|
||||
|
||||
case '0':
|
||||
case 'false':
|
||||
case 'no':
|
||||
case 'off':
|
||||
return false;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException(\sprintf('Invalid %s value: %s. Accepted values: %s', $name, $value, $acceptedValues));
|
||||
}
|
||||
};
|
||||
};
|
||||
$semicolonsSuppressReturnParser = function (string $name, string $acceptedValues): callable {
|
||||
return function (string $value) use ($name, $acceptedValues) {
|
||||
switch (\strtolower($value)) {
|
||||
case '1':
|
||||
case 'true':
|
||||
case 'yes':
|
||||
case 'on':
|
||||
return true;
|
||||
|
||||
case '0':
|
||||
case 'false':
|
||||
case 'no':
|
||||
case 'off':
|
||||
return false;
|
||||
|
||||
case Configuration::SEMICOLONS_SUPPRESS_RETURN_DOUBLE:
|
||||
return Configuration::SEMICOLONS_SUPPRESS_RETURN_DOUBLE;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException(\sprintf('Invalid %s value: %s. Accepted values: %s', $name, $value, $acceptedValues));
|
||||
}
|
||||
};
|
||||
};
|
||||
$enumParser = function (string $name, array $values, string $acceptedValues): callable {
|
||||
return function (string $value) use ($name, $values, $acceptedValues): string {
|
||||
if (!\in_array($value, $values, true)) {
|
||||
throw new \InvalidArgumentException(\sprintf('Invalid %s value: %s. Accepted values: %s', $name, $value, $acceptedValues));
|
||||
}
|
||||
|
||||
return $value;
|
||||
};
|
||||
};
|
||||
$configEnumParser = function (string $name, array $values, string $acceptedValues): callable {
|
||||
return function (string $value) use ($name, $values, $acceptedValues): string {
|
||||
if (\in_array($value, $values, true)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
try {
|
||||
$resolved = $this->resolveConfigurationConstant($value);
|
||||
} catch (\Throwable $e) {
|
||||
throw new \InvalidArgumentException(\sprintf('Invalid %s value: %s. Accepted values: %s', $name, $value, $acceptedValues), 0, $e);
|
||||
}
|
||||
|
||||
if (!\is_string($resolved) || !\in_array($resolved, $values, true)) {
|
||||
throw new \InvalidArgumentException(\sprintf('Invalid %s value: %s. Accepted values: %s', $name, $value, $acceptedValues));
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
};
|
||||
};
|
||||
|
||||
$this->options = [
|
||||
'verbosity' => [
|
||||
'name' => 'verbosity',
|
||||
'acceptedValues' => [
|
||||
Configuration::VERBOSITY_QUIET,
|
||||
Configuration::VERBOSITY_NORMAL,
|
||||
Configuration::VERBOSITY_VERBOSE,
|
||||
Configuration::VERBOSITY_VERY_VERBOSE,
|
||||
Configuration::VERBOSITY_DEBUG,
|
||||
],
|
||||
'parser' => $configEnumParser('verbosity', [
|
||||
Configuration::VERBOSITY_QUIET,
|
||||
Configuration::VERBOSITY_NORMAL,
|
||||
Configuration::VERBOSITY_VERBOSE,
|
||||
Configuration::VERBOSITY_VERY_VERBOSE,
|
||||
Configuration::VERBOSITY_DEBUG,
|
||||
], 'quiet|normal|verbose|very_verbose|debug'),
|
||||
'getter' => function () use ($config): string {
|
||||
return $config->verbosity();
|
||||
},
|
||||
'setter' => function (string $value) use ($config): void {
|
||||
$config->setVerbosity($value);
|
||||
},
|
||||
'refresh' => true,
|
||||
],
|
||||
'useunicode' => [
|
||||
'name' => 'useUnicode',
|
||||
'acceptedValues' => ['on', 'off'],
|
||||
'parser' => $booleanParser('useUnicode', 'on|off'),
|
||||
'getter' => function () use ($config): bool {
|
||||
return $config->useUnicode();
|
||||
},
|
||||
'setter' => function (bool $value) use ($config): void {
|
||||
$config->setUseUnicode($value);
|
||||
},
|
||||
'refresh' => false,
|
||||
],
|
||||
'errorlogginglevel' => [
|
||||
'name' => 'errorLoggingLevel',
|
||||
'acceptedValues' => ['<php-expression>'],
|
||||
'parser' => function (string $value): int {
|
||||
if (\preg_match('/^\d+$/', $value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
try {
|
||||
$resolved = $this->getShell()->execute($value, true);
|
||||
} catch (\Throwable $e) {
|
||||
throw new \InvalidArgumentException(\sprintf('Invalid errorLoggingLevel value: %s. Accepted values: <php-expression>', $value), 0, $e);
|
||||
}
|
||||
|
||||
if (!\is_int($resolved)) {
|
||||
throw new \InvalidArgumentException(\sprintf('Invalid errorLoggingLevel value: %s. Accepted values: <php-expression>', $value));
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
},
|
||||
'getter' => function () use ($config): string {
|
||||
return $this->formatErrorLoggingLevel($config->errorLoggingLevel());
|
||||
},
|
||||
'setter' => function (int $value) use ($config): void {
|
||||
$config->setErrorLoggingLevel($value);
|
||||
},
|
||||
'refresh' => false,
|
||||
],
|
||||
'clipboardcommand' => [
|
||||
'name' => 'clipboardCommand',
|
||||
'acceptedValues' => ['auto', '<command>'],
|
||||
'parser' => function (string $value): ?string {
|
||||
return \strtolower($value) === 'auto' ? null : $value;
|
||||
},
|
||||
'getter' => function () use ($config): string {
|
||||
return $config->clipboardCommand() ?? 'auto';
|
||||
},
|
||||
'setter' => function (?string $value) use ($config): void {
|
||||
$config->setClipboardCommand($value);
|
||||
},
|
||||
'refresh' => false,
|
||||
],
|
||||
'useosc52clipboard' => [
|
||||
'name' => 'useOsc52Clipboard',
|
||||
'acceptedValues' => ['on', 'off'],
|
||||
'parser' => $booleanParser('useOsc52Clipboard', 'on|off'),
|
||||
'getter' => function () use ($config): bool {
|
||||
return $config->useOsc52Clipboard();
|
||||
},
|
||||
'setter' => function (bool $value) use ($config): void {
|
||||
$config->setUseOsc52Clipboard($value);
|
||||
},
|
||||
'refresh' => false,
|
||||
],
|
||||
'colormode' => [
|
||||
'name' => 'colorMode',
|
||||
'acceptedValues' => [
|
||||
Configuration::COLOR_MODE_AUTO,
|
||||
Configuration::COLOR_MODE_FORCED,
|
||||
Configuration::COLOR_MODE_DISABLED,
|
||||
],
|
||||
'parser' => $configEnumParser('colorMode', [
|
||||
Configuration::COLOR_MODE_AUTO,
|
||||
Configuration::COLOR_MODE_FORCED,
|
||||
Configuration::COLOR_MODE_DISABLED,
|
||||
], 'auto|forced|disabled'),
|
||||
'getter' => function () use ($config): string {
|
||||
return $config->colorMode();
|
||||
},
|
||||
'setter' => function (string $value) use ($config): void {
|
||||
$config->setColorMode($value);
|
||||
},
|
||||
'refresh' => true,
|
||||
],
|
||||
'theme' => [
|
||||
'name' => 'theme',
|
||||
'acceptedValues' => Theme::BUILTIN_THEMES,
|
||||
'parser' => $enumParser('theme', Theme::BUILTIN_THEMES, \implode('|', Theme::BUILTIN_THEMES)),
|
||||
'getter' => function () use ($config): string {
|
||||
return $config->theme()->getName() ?? 'custom';
|
||||
},
|
||||
'setter' => function (string $value) use ($config): bool {
|
||||
$before = $config->theme();
|
||||
$config->setTheme($value);
|
||||
|
||||
return !$before->equals($config->theme());
|
||||
},
|
||||
'refresh' => true,
|
||||
],
|
||||
'pager' => [
|
||||
'name' => 'pager',
|
||||
'acceptedValues' => ['default', 'off', '<command>'],
|
||||
'parser' => function (string $value) {
|
||||
switch (\strtolower($value)) {
|
||||
case 'default':
|
||||
case 'on':
|
||||
case 'yes':
|
||||
case 'true':
|
||||
case '1':
|
||||
return null;
|
||||
case 'off':
|
||||
case 'no':
|
||||
case 'false':
|
||||
case '0':
|
||||
return false;
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
},
|
||||
'getter' => function () use ($config): string {
|
||||
$pager = $config->getPager();
|
||||
|
||||
if ($pager === false) {
|
||||
return 'off';
|
||||
}
|
||||
|
||||
if ($pager === null) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
if (\is_string($pager)) {
|
||||
return $pager;
|
||||
}
|
||||
|
||||
return \get_class($pager);
|
||||
},
|
||||
'setter' => function ($value) use ($config): void {
|
||||
if ($value === null) {
|
||||
$config->setDefaultPager();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$config->setPager($value);
|
||||
},
|
||||
'refresh' => true,
|
||||
],
|
||||
'requiresemicolons' => [
|
||||
'name' => 'requireSemicolons',
|
||||
'acceptedValues' => ['on', 'off'],
|
||||
'parser' => $booleanParser('requireSemicolons', 'on|off'),
|
||||
'getter' => function () use ($config): bool {
|
||||
return $config->requireSemicolons();
|
||||
},
|
||||
'setter' => function (bool $value) use ($config): void {
|
||||
$config->setRequireSemicolons($value);
|
||||
},
|
||||
'refresh' => true,
|
||||
],
|
||||
'semicolonssuppressreturn' => [
|
||||
'name' => 'semicolonsSuppressReturn',
|
||||
'acceptedValues' => ['on', 'off', Configuration::SEMICOLONS_SUPPRESS_RETURN_DOUBLE],
|
||||
'parser' => $semicolonsSuppressReturnParser('semicolonsSuppressReturn', 'on|off|double'),
|
||||
'getter' => function () use ($config) {
|
||||
return $config->semicolonsSuppressReturn();
|
||||
},
|
||||
'setter' => function ($value) use ($config): void {
|
||||
$config->setSemicolonsSuppressReturn($value);
|
||||
},
|
||||
'refresh' => false,
|
||||
],
|
||||
'usebracketedpaste' => [
|
||||
'name' => 'useBracketedPaste',
|
||||
'acceptedValues' => ['on', 'off'],
|
||||
'parser' => $booleanParser('useBracketedPaste', 'on|off'),
|
||||
'getter' => function () use ($config): bool {
|
||||
return $config->useBracketedPaste();
|
||||
},
|
||||
'setter' => function (bool $value) use ($config): void {
|
||||
$config->setUseBracketedPaste($value);
|
||||
},
|
||||
'refresh' => true,
|
||||
],
|
||||
'usesyntaxhighlighting' => [
|
||||
'name' => 'useSyntaxHighlighting',
|
||||
'acceptedValues' => ['on', 'off'],
|
||||
'parser' => $booleanParser('useSyntaxHighlighting', 'on|off'),
|
||||
'getter' => function () use ($config): bool {
|
||||
return $config->useSyntaxHighlighting();
|
||||
},
|
||||
'setter' => function (bool $value) use ($config): void {
|
||||
$config->setUseSyntaxHighlighting($value);
|
||||
},
|
||||
'refresh' => true,
|
||||
],
|
||||
'usesuggestions' => [
|
||||
'name' => 'useSuggestions',
|
||||
'acceptedValues' => ['on', 'off'],
|
||||
'parser' => $booleanParser('useSuggestions', 'on|off'),
|
||||
'getter' => function () use ($config): bool {
|
||||
return $config->useSuggestions();
|
||||
},
|
||||
'setter' => function (bool $value) use ($config): void {
|
||||
$config->setUseSuggestions($value);
|
||||
},
|
||||
'refresh' => true,
|
||||
],
|
||||
];
|
||||
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
protected function getOption(string $key): ?array
|
||||
{
|
||||
return $this->getOptions()[\strtolower($key)] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getOptionNames(): array
|
||||
{
|
||||
return \array_map(
|
||||
fn (array $option): string => $option['name'],
|
||||
\array_values($this->getOptions())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
protected function formatValue($value): string
|
||||
{
|
||||
if (\is_bool($value)) {
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
protected function formatAcceptedValues(array $option): string
|
||||
{
|
||||
return OutputFormatter::escape(\implode('|', $option['acceptedValues']));
|
||||
}
|
||||
|
||||
protected function formatErrorLoggingLevel(int $value): string
|
||||
{
|
||||
if ($value === 0) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
foreach ($this->getErrorLoggingConstants() as $name => $constantValue) {
|
||||
if ($value === $constantValue) {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
|
||||
$allMask = $this->getErrorLoggingAllMask();
|
||||
if (($value & $allMask) === $value) {
|
||||
$included = $this->formatErrorLoggingFlags($value);
|
||||
$missingValue = $allMask & ~$value;
|
||||
$missing = $this->formatErrorLoggingFlags($missingValue);
|
||||
|
||||
if ($included !== null && $missing !== null && $this->countErrorLoggingFlags($missingValue) < $this->countErrorLoggingFlags($value)) {
|
||||
return 'E_ALL & ~'.$this->wrapErrorLoggingFlags($missing);
|
||||
}
|
||||
|
||||
if ($included !== null) {
|
||||
return $included;
|
||||
}
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
protected function formatOptionName(string $name): string
|
||||
{
|
||||
return \sprintf('<info>%s</info>', $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $names
|
||||
*/
|
||||
protected function formatOptionNames(array $names): string
|
||||
{
|
||||
return \implode(', ', \array_map(fn (string $name): string => $this->formatOptionName($name), $names));
|
||||
}
|
||||
|
||||
protected function unsupportedMessage(string $key): string
|
||||
{
|
||||
return \sprintf('Configuration option `%s` is not runtime-configurable.', $key);
|
||||
}
|
||||
|
||||
protected function getConfig(): Configuration
|
||||
{
|
||||
if ($this->config === null) {
|
||||
throw new \RuntimeException('Configuration not available.');
|
||||
}
|
||||
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[] Error logging constants keyed by name
|
||||
*/
|
||||
private function getErrorLoggingConstants(): array
|
||||
{
|
||||
$names = [
|
||||
'E_ALL',
|
||||
'E_ERROR',
|
||||
'E_WARNING',
|
||||
'E_PARSE',
|
||||
'E_NOTICE',
|
||||
'E_CORE_ERROR',
|
||||
'E_CORE_WARNING',
|
||||
'E_COMPILE_ERROR',
|
||||
'E_COMPILE_WARNING',
|
||||
'E_USER_ERROR',
|
||||
'E_USER_WARNING',
|
||||
'E_USER_NOTICE',
|
||||
'E_STRICT',
|
||||
'E_RECOVERABLE_ERROR',
|
||||
'E_DEPRECATED',
|
||||
'E_USER_DEPRECATED',
|
||||
];
|
||||
|
||||
$constants = [];
|
||||
|
||||
foreach ($names as $name) {
|
||||
if (\defined($name)) {
|
||||
/** @var int $value */
|
||||
$value = \constant($name);
|
||||
$constants[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $constants;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[] Error logging flag constants keyed by name, excluding E_ALL
|
||||
*/
|
||||
private function getErrorLoggingFlagConstants(): array
|
||||
{
|
||||
$constants = $this->getErrorLoggingConstants();
|
||||
unset($constants['E_ALL']);
|
||||
|
||||
return $constants;
|
||||
}
|
||||
|
||||
private function getErrorLoggingAllMask(): int
|
||||
{
|
||||
return \PHP_VERSION_ID < 80400 ? (\E_ALL | \E_STRICT) : \E_ALL;
|
||||
}
|
||||
|
||||
private function formatErrorLoggingFlags(int $value): ?string
|
||||
{
|
||||
if ($value === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
$covered = 0;
|
||||
|
||||
foreach ($this->getErrorLoggingFlagConstants() as $name => $constantValue) {
|
||||
if ($constantValue !== 0 && ($value & $constantValue) === $constantValue) {
|
||||
$parts[] = $name;
|
||||
$covered |= $constantValue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($parts === [] || $covered !== $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return \implode(' | ', $parts);
|
||||
}
|
||||
|
||||
private function countErrorLoggingFlags(int $value): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
foreach ($this->getErrorLoggingFlagConstants() as $constantValue) {
|
||||
if ($constantValue !== 0 && ($value & $constantValue) === $constantValue) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function wrapErrorLoggingFlags(string $expression): string
|
||||
{
|
||||
return \strpos($expression, ' | ') === false ? $expression : '('.$expression.')';
|
||||
}
|
||||
|
||||
private function resolveConfigurationConstant(string $value): string
|
||||
{
|
||||
if (!\preg_match('/^\\\\?(?:Psy\\\\)?Configuration::([A-Z_]+)$/', $value, $matches)) {
|
||||
throw new \InvalidArgumentException('Unsupported configuration constant expression.');
|
||||
}
|
||||
|
||||
$constant = 'Psy\\Configuration::'.$matches[1];
|
||||
|
||||
if (!\defined($constant)) {
|
||||
throw new \InvalidArgumentException('Unknown configuration constant.');
|
||||
}
|
||||
|
||||
$resolved = \constant($constant);
|
||||
|
||||
if (!\is_string($resolved)) {
|
||||
throw new \InvalidArgumentException('Configuration constant does not resolve to a string value.');
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
}
|
||||
69
vendor/psy/psysh/src/Command/Config/ConfigGetCommand.php
vendored
Normal file
69
vendor/psy/psysh/src/Command/Config/ConfigGetCommand.php
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command\Config;
|
||||
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Print the current value for a runtime-configurable PsySH setting.
|
||||
*/
|
||||
class ConfigGetCommand extends AbstractConfigCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('config-get')
|
||||
->setDefinition([
|
||||
new InputArgument('key', InputArgument::OPTIONAL, 'Runtime-configurable option to inspect.'),
|
||||
])
|
||||
->setDescription('Print the current value for one runtime-configurable PsySH setting.');
|
||||
}
|
||||
|
||||
public function asText(): string
|
||||
{
|
||||
return \implode("\n", [
|
||||
'<comment>Usage:</comment>',
|
||||
' config get \\<key>',
|
||||
'',
|
||||
'<comment>Help:</comment>',
|
||||
' Print the current value for one runtime-configurable PsySH setting.',
|
||||
'',
|
||||
'<comment>Examples:</comment>',
|
||||
' <return>>>> config get verbosity</return>',
|
||||
' <return>>>> config get theme</return>',
|
||||
'',
|
||||
'<comment>Supported Options:</comment>',
|
||||
' '.$this->formatOptionNames($this->getOptionNames()),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$key = $input->getArgument('key');
|
||||
if ($key === null) {
|
||||
throw new \InvalidArgumentException('Please specify a runtime-configurable option to inspect.');
|
||||
}
|
||||
|
||||
$option = $this->getOption($key);
|
||||
if ($option === null) {
|
||||
$output->writeln(\sprintf('<error>%s</error>', $this->unsupportedMessage((string) $key)));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln($this->formatValue($option['getter']()));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
55
vendor/psy/psysh/src/Command/Config/ConfigListCommand.php
vendored
Normal file
55
vendor/psy/psysh/src/Command/Config/ConfigListCommand.php
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command\Config;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Show runtime-configurable PsySH settings and their current values.
|
||||
*/
|
||||
class ConfigListCommand extends AbstractConfigCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('config-list')
|
||||
->setDescription('Show runtime-configurable PsySH settings and their current values.');
|
||||
}
|
||||
|
||||
public function asText(): string
|
||||
{
|
||||
return \implode("\n", [
|
||||
'<comment>Usage:</comment>',
|
||||
' config list',
|
||||
'',
|
||||
'<comment>Help:</comment>',
|
||||
' Show runtime-configurable PsySH settings and their current values.',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$table = $this->getTable($output);
|
||||
|
||||
foreach ($this->getOptions() as $option) {
|
||||
$table->addRow([
|
||||
$this->formatOptionName($option['name']),
|
||||
$this->formatValue($option['getter']()),
|
||||
]);
|
||||
}
|
||||
|
||||
$table->render();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
134
vendor/psy/psysh/src/Command/Config/ConfigSetCommand.php
vendored
Normal file
134
vendor/psy/psysh/src/Command/Config/ConfigSetCommand.php
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command\Config;
|
||||
|
||||
use Psy\Input\CodeArgument;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Update a runtime-configurable PsySH setting for the current session.
|
||||
*/
|
||||
class ConfigSetCommand extends AbstractConfigCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('config-set')
|
||||
->setDefinition([
|
||||
new InputArgument('key', InputArgument::OPTIONAL, 'Runtime-configurable option to update.'),
|
||||
new CodeArgument('value', CodeArgument::OPTIONAL, 'New runtime value for the selected option.'),
|
||||
])
|
||||
->setDescription('Update one runtime-configurable PsySH setting for the current session.');
|
||||
}
|
||||
|
||||
public function asText(): string
|
||||
{
|
||||
return \implode("\n", [
|
||||
'<comment>Usage:</comment>',
|
||||
' config set \\<key> \\<value>',
|
||||
'',
|
||||
'<comment>Help:</comment>',
|
||||
' Set a runtime-configurable PsySH setting for the current session.',
|
||||
'',
|
||||
'<comment>Examples:</comment>',
|
||||
' <return>>>> config set verbosity debug</return>',
|
||||
' <return>>>> config set pager off</return>',
|
||||
' <return>>>> config set \\<key> --help</return>',
|
||||
'',
|
||||
'<comment>Supported Options:</comment>',
|
||||
$this->renderSettableKeys(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function asTextForInput(InputInterface $input): string
|
||||
{
|
||||
$key = $input->getArgument('key');
|
||||
|
||||
if ($key === null) {
|
||||
return $this->asText();
|
||||
}
|
||||
|
||||
$option = $this->getOption((string) $key);
|
||||
|
||||
if ($option === null) {
|
||||
return $this->asText();
|
||||
}
|
||||
|
||||
return \implode("\n", [
|
||||
'<comment>Usage:</comment>',
|
||||
\sprintf(' config set %s \\<value>', $option['name']),
|
||||
'',
|
||||
'<comment>Help:</comment>',
|
||||
\sprintf(' Set %s for the current session.', $this->formatOptionName($option['name'])),
|
||||
'',
|
||||
'<comment>Accepted Values:</comment>',
|
||||
' '.$this->formatAcceptedValues($option),
|
||||
'',
|
||||
'<comment>Current Value:</comment>',
|
||||
' '.$this->formatValue($option['getter']()),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$key = $input->getArgument('key');
|
||||
if ($key === null) {
|
||||
throw new \InvalidArgumentException('Please specify a runtime-configurable option to update.');
|
||||
}
|
||||
|
||||
$option = $this->getOption($key);
|
||||
if ($option === null) {
|
||||
$output->writeln(\sprintf('<error>%s</error>', $this->unsupportedMessage((string) $key)));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$rawValue = $input->getArgument('value');
|
||||
if ($rawValue === null) {
|
||||
throw new \InvalidArgumentException(\sprintf('Please specify a value for `%s`. Accepted values: %s', $option['name'], $this->formatAcceptedValues($option)));
|
||||
}
|
||||
|
||||
try {
|
||||
$value = $option['parser']((string) $rawValue);
|
||||
$changed = $option['setter']($value);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$output->writeln(\sprintf('<error>%s</error>', $e->getMessage()));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($option['refresh'] && $changed !== false) {
|
||||
$this->getShell()->applyRuntimeConfigChange($option['name']);
|
||||
}
|
||||
|
||||
$output->writeln(\sprintf(
|
||||
'<info>%s</info> = <return>%s</return>',
|
||||
$option['name'],
|
||||
$this->formatValue($option['getter']())
|
||||
));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function renderSettableKeys(): string
|
||||
{
|
||||
$lines = [];
|
||||
|
||||
foreach ($this->getOptions() as $option) {
|
||||
$lines[] = \sprintf(' %s (%s)', $this->formatOptionName($option['name']), $this->formatAcceptedValues($option));
|
||||
}
|
||||
|
||||
return \implode("\n", $lines);
|
||||
}
|
||||
}
|
||||
377
vendor/psy/psysh/src/Command/ConfigCommand.php
vendored
Normal file
377
vendor/psy/psysh/src/Command/ConfigCommand.php
vendored
Normal file
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use Psy\Command\Config\AbstractConfigCommand;
|
||||
use Psy\Command\Config\ConfigGetCommand;
|
||||
use Psy\Command\Config\ConfigListCommand;
|
||||
use Psy\Command\Config\ConfigSetCommand;
|
||||
use Psy\CommandArgumentCompletionAware;
|
||||
use Psy\Completion\AnalysisResult;
|
||||
use Psy\Completion\FuzzyMatcher;
|
||||
use Psy\Input\CodeArgument;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\StringInput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Inspect and update runtime-configurable settings for the current shell session.
|
||||
*/
|
||||
class ConfigCommand extends AbstractConfigCommand implements CommandArgumentCompletionAware
|
||||
{
|
||||
private const ACTIONS = ['list', 'get', 'set'];
|
||||
|
||||
/** @var array{supported: bool, completions: string[]}|null */
|
||||
private ?array $lastCompletionResult = null;
|
||||
private string $lastCompletionInput = '';
|
||||
|
||||
private string $defaultHelp = '';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->defaultHelp = \implode("\n", [
|
||||
'Inspect or update runtime-configurable PsySH settings for the current session.',
|
||||
'',
|
||||
'e.g.',
|
||||
'<return>>>> config list</return>',
|
||||
'<return>>>> config get verbosity</return>',
|
||||
'<return>>>> config set verbosity debug</return>',
|
||||
'<return>>>> config set pager off</return>',
|
||||
'<return>>>> config set clipboardCommand auto</return>',
|
||||
'',
|
||||
'Runtime-configurable keys include '.$this->formatOptionNames([
|
||||
'verbosity',
|
||||
'useUnicode',
|
||||
'errorLoggingLevel',
|
||||
'clipboardCommand',
|
||||
'useOsc52Clipboard',
|
||||
'colorMode',
|
||||
'theme',
|
||||
'pager',
|
||||
'requireSemicolons',
|
||||
'semicolonsSuppressReturn',
|
||||
'useBracketedPaste',
|
||||
'useSyntaxHighlighting',
|
||||
'useSuggestions',
|
||||
]).'.',
|
||||
]);
|
||||
|
||||
$this
|
||||
->setName('config')
|
||||
->setDefinition([
|
||||
new InputArgument('action', InputArgument::OPTIONAL, 'Action: list, get, or set.', 'list'),
|
||||
new InputArgument('key', InputArgument::OPTIONAL, 'Runtime-configurable option to inspect or update.'),
|
||||
new CodeArgument('value', CodeArgument::OPTIONAL, 'New value when using `set`.'),
|
||||
])
|
||||
->setDescription('Inspect or update runtime-configurable PsySH settings for the current session.')
|
||||
->setHelp($this->defaultHelp);
|
||||
}
|
||||
|
||||
public function run(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
if ($input->hasParameterOption(['--help', '-h'], true)) {
|
||||
$output->writeln($this->asTextForInput($input));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return parent::run($input, $output);
|
||||
}
|
||||
|
||||
public function asTextForInput(InputInterface $input): string
|
||||
{
|
||||
$action = $this->getActionFromInput($input);
|
||||
|
||||
if ($action === '') {
|
||||
return $this->asText();
|
||||
}
|
||||
|
||||
$command = $this->createChildCommand($action);
|
||||
|
||||
if ($command === null) {
|
||||
return $this->asText();
|
||||
}
|
||||
|
||||
return $command->asTextForInput($this->createChildInput($command, $action, $this->rawArguments($input)));
|
||||
}
|
||||
|
||||
public function getArgumentCompletions(AnalysisResult $analysis): array
|
||||
{
|
||||
return $this->resolveArgumentCompletion($analysis)['completions'];
|
||||
}
|
||||
|
||||
public function supportsArgumentCompletion(AnalysisResult $analysis): bool
|
||||
{
|
||||
return $this->resolveArgumentCompletion($analysis)['supported'];
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$action = \strtolower((string) $input->getArgument('action'));
|
||||
$command = $this->createChildCommand($action);
|
||||
|
||||
if ($command === null) {
|
||||
throw new \InvalidArgumentException(\sprintf('Unknown config action: %s. Expected list, get, or set.', $action));
|
||||
}
|
||||
|
||||
return $command->run($this->createChildInput($command, $action, [
|
||||
$action,
|
||||
(string) $input->getArgument('key'),
|
||||
(string) $input->getArgument('value'),
|
||||
]), $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $arguments
|
||||
*/
|
||||
private function createChildInput(Command $command, string $action, array $arguments): ArrayInput
|
||||
{
|
||||
$parameters = [];
|
||||
|
||||
switch ($action) {
|
||||
case 'get':
|
||||
if (isset($arguments[1]) && $arguments[1] !== '') {
|
||||
$parameters['key'] = $arguments[1];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'set':
|
||||
if (isset($arguments[1]) && $arguments[1] !== '') {
|
||||
$parameters['key'] = $arguments[1];
|
||||
}
|
||||
if (isset($arguments[2]) && $arguments[2] !== '') {
|
||||
$parameters['value'] = $arguments[2];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$input = new ArrayInput($parameters, $command->getDefinition());
|
||||
$input->setInteractive(false);
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
private function createChildCommand(string $action): ?Command
|
||||
{
|
||||
switch ($action) {
|
||||
case '':
|
||||
case 'list':
|
||||
$command = new ConfigListCommand();
|
||||
break;
|
||||
|
||||
case 'get':
|
||||
$command = new ConfigGetCommand();
|
||||
break;
|
||||
|
||||
case 'set':
|
||||
$command = new ConfigSetCommand();
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
$command->setConfiguration($this->getConfig());
|
||||
$command->setApplication($this->getApplication());
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
private function getActionFromInput(InputInterface $input): string
|
||||
{
|
||||
$arguments = $this->rawArguments($input);
|
||||
|
||||
return \strtolower($arguments[0] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract positional arguments from the raw input string.
|
||||
*
|
||||
* Symfony's Input classes don't expose raw tokens after parsing, so we
|
||||
* re-tokenize __toString() output to recover them for child command routing.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function rawArguments(InputInterface $input): array
|
||||
{
|
||||
if (!$input instanceof ArrayInput && !$input instanceof StringInput) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->tokenizeArguments($input->__toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string[], 1: bool}
|
||||
*/
|
||||
private function parseCompletionInput(string $input): array
|
||||
{
|
||||
$trimmed = \rtrim($input);
|
||||
|
||||
return [$this->tokenizeArguments($trimmed), $trimmed !== $input];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenize an input string into positional arguments, skipping options.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function tokenizeArguments(string $input): array
|
||||
{
|
||||
if ($input === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
\preg_match_all('/"[^"]*"|\'[^\']*\'|\S+/', $input, $matches);
|
||||
|
||||
$arguments = [];
|
||||
|
||||
foreach ($matches[0] as $token) {
|
||||
if ($token === '--') {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($token !== '' && $token[0] === '-') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$arguments[] = $this->trimQuotes($token);
|
||||
}
|
||||
|
||||
$first = $arguments[0] ?? null;
|
||||
if ($first === $this->getName() || \in_array($first, $this->getAliases(), true)) {
|
||||
\array_shift($arguments);
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $arguments
|
||||
*/
|
||||
private function isCompletingSetValue(array $arguments, bool $hasTrailingSpace): bool
|
||||
{
|
||||
$count = \count($arguments);
|
||||
|
||||
if ($count < 2 || $count > 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($count === 2 && $hasTrailingSpace) || ($count === 3 && !$hasTrailingSpace);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{supported: bool, completions: string[]}
|
||||
*/
|
||||
private function resolveArgumentCompletion(AnalysisResult $analysis): array
|
||||
{
|
||||
if ($this->lastCompletionResult !== null && $this->lastCompletionInput === $analysis->input) {
|
||||
return $this->lastCompletionResult;
|
||||
}
|
||||
|
||||
$this->lastCompletionInput = $analysis->input;
|
||||
|
||||
return $this->lastCompletionResult = $this->doResolveArgumentCompletion($analysis->input);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{supported: bool, completions: string[]}
|
||||
*/
|
||||
private function doResolveArgumentCompletion(string $input): array
|
||||
{
|
||||
[$arguments, $hasTrailingSpace] = $this->parseCompletionInput($input);
|
||||
$count = \count($arguments);
|
||||
$action = \strtolower($arguments[0] ?? '');
|
||||
|
||||
if ($count === 0 || ($count === 1 && !$hasTrailingSpace)) {
|
||||
return ['supported' => true, 'completions' => self::ACTIONS];
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'list':
|
||||
return ['supported' => true, 'completions' => []];
|
||||
|
||||
case 'get':
|
||||
case 'set':
|
||||
// Completing the key name (cursor on or just after argument position 2)
|
||||
if ($count <= 2 && ($count === 1 || !$hasTrailingSpace)) {
|
||||
return ['supported' => true, 'completions' => $this->getOptionNames()];
|
||||
}
|
||||
|
||||
if ($action !== 'set') {
|
||||
return ['supported' => true, 'completions' => []];
|
||||
}
|
||||
|
||||
if (!$this->isCompletingSetValue($arguments, $hasTrailingSpace)) {
|
||||
return ['supported' => true, 'completions' => []];
|
||||
}
|
||||
|
||||
return $this->resolveSetValueCompletion($arguments, $hasTrailingSpace);
|
||||
|
||||
default:
|
||||
return ['supported' => true, 'completions' => self::ACTIONS];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $arguments
|
||||
*
|
||||
* @return array{supported: bool, completions: string[]}
|
||||
*/
|
||||
private function resolveSetValueCompletion(array $arguments, bool $hasTrailingSpace): array
|
||||
{
|
||||
$key = $arguments[1];
|
||||
$option = $this->getOption($key);
|
||||
if ($option === null) {
|
||||
return ['supported' => false, 'completions' => []];
|
||||
}
|
||||
|
||||
$acceptsFreeForm = false;
|
||||
$completions = [];
|
||||
foreach ($option['acceptedValues'] as $value) {
|
||||
if ($value !== '' && $value[0] === '<') {
|
||||
$acceptsFreeForm = true;
|
||||
} else {
|
||||
$completions[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$acceptsFreeForm) {
|
||||
return ['supported' => $completions !== [], 'completions' => $completions];
|
||||
}
|
||||
|
||||
$valuePrefix = $hasTrailingSpace ? '' : ($arguments[2] ?? '');
|
||||
|
||||
if ($valuePrefix === '') {
|
||||
return ['supported' => $completions !== [], 'completions' => $completions];
|
||||
}
|
||||
|
||||
if (FuzzyMatcher::filter($valuePrefix, $completions) !== []) {
|
||||
return ['supported' => true, 'completions' => $completions];
|
||||
}
|
||||
|
||||
return ['supported' => false, 'completions' => []];
|
||||
}
|
||||
|
||||
private function trimQuotes(string $token): string
|
||||
{
|
||||
$quote = $token[0] ?? '';
|
||||
|
||||
if (($quote === '"' || $quote === '\'') && \substr($token, -1) === $quote) {
|
||||
return \substr($token, 1, -1);
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
125
vendor/psy/psysh/src/Command/CopyCommand.php
vendored
Normal file
125
vendor/psy/psysh/src/Command/CopyCommand.php
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\Clipboard\ClipboardMethod;
|
||||
use Psy\Clipboard\NullClipboardMethod;
|
||||
use Psy\Configuration;
|
||||
use Psy\Input\CodeArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Copy a value to the clipboard.
|
||||
*/
|
||||
class CopyCommand extends ReflectingCommand
|
||||
{
|
||||
private ?Configuration $config = null;
|
||||
|
||||
/**
|
||||
* Set the configuration instance.
|
||||
*
|
||||
* @param Configuration $config
|
||||
*/
|
||||
public function setConfiguration(Configuration $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('copy')
|
||||
->setDefinition([
|
||||
new CodeArgument('expression', CodeArgument::OPTIONAL, 'Expression to copy.'),
|
||||
])
|
||||
->setDescription('Copy a value to the clipboard.')
|
||||
->setHelp(
|
||||
<<<'HELP'
|
||||
Copy a value to the clipboard.
|
||||
|
||||
When given:
|
||||
- an expression, copy the exported value of the expression to the clipboard.
|
||||
- no arguments, copy the last evaluated result (<info>$_</info>) to the clipboard.
|
||||
|
||||
e.g.
|
||||
<return>>>> copy new Foo()</return>
|
||||
<return>>>> copy User::all()->toArray()</return>
|
||||
<return>>>> copy</return>
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int 0 if everything went fine, or an exit code
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$expression = $input->getArgument('expression');
|
||||
$value = $expression === null ? $this->context->get('_') : $this->resolveCode($expression);
|
||||
|
||||
if (\is_object($value)) {
|
||||
$this->setCommandScopeVariables(new \ReflectionObject($value));
|
||||
}
|
||||
|
||||
if (!$this->getClipboardMethod()->copy($this->exportValue($value, $output), $output)) {
|
||||
$output->writeln('<error>Unable to copy value to clipboard.</error>');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<info>Copied to clipboard.</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function getClipboardMethod(): ClipboardMethod
|
||||
{
|
||||
return $this->config ? $this->config->getClipboard() : new NullClipboardMethod(false);
|
||||
}
|
||||
|
||||
private function exportValue($value, OutputInterface $output): string
|
||||
{
|
||||
$export = '';
|
||||
$warnings = [];
|
||||
\set_error_handler(static function (int $errno, string $errstr) use (&$warnings): bool {
|
||||
$warnings[$errstr] = true;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
try {
|
||||
$export = (string) \var_export($value, true);
|
||||
} finally {
|
||||
\restore_error_handler();
|
||||
}
|
||||
|
||||
foreach (\array_keys($warnings) as $warning) {
|
||||
if ($warning === 'var_export does not handle circular references') {
|
||||
$output->writeln('<warning>Value contains circular references; copied export may be incomplete.</warning>');
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$output->writeln(\sprintf('<warning>%s</warning>', $warning));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $export;
|
||||
}
|
||||
}
|
||||
336
vendor/psy/psysh/src/Command/DocCommand.php
vendored
Normal file
336
vendor/psy/psysh/src/Command/DocCommand.php
vendored
Normal file
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\Configuration;
|
||||
use Psy\Formatter\DocblockFormatter;
|
||||
use Psy\Formatter\ManualFormatter;
|
||||
use Psy\Formatter\SignatureFormatter;
|
||||
use Psy\Input\CodeArgument;
|
||||
use Psy\ManualUpdater\ManualUpdate;
|
||||
use Psy\Reflection\ReflectionConstant;
|
||||
use Psy\Reflection\ReflectionLanguageConstruct;
|
||||
use Psy\Util\Tty;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
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}';
|
||||
|
||||
private ?Configuration $config = null;
|
||||
|
||||
/**
|
||||
* Set the configuration instance.
|
||||
*
|
||||
* @param \Psy\Configuration $config
|
||||
*/
|
||||
public function setConfiguration(Configuration $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$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 InputOption('update-manual', null, InputOption::VALUE_OPTIONAL, 'Download and install the latest PHP manual (optional language code)', false),
|
||||
new CodeArgument('target', CodeArgument::OPTIONAL, '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>
|
||||
<return>>>> doc --update-manual</return>
|
||||
<return>>>> doc --update-manual=fr</return>
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int 0 if everything went fine, or an exit code
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$shellOutput = $this->shellOutput($output);
|
||||
|
||||
if ($input->getOption('update-manual') !== false) {
|
||||
return $this->handleUpdateManual($input, $output);
|
||||
}
|
||||
|
||||
$value = $input->getArgument('target');
|
||||
if (!$value) {
|
||||
throw new RuntimeException('Not enough arguments (missing: "target").');
|
||||
}
|
||||
|
||||
if (ReflectionLanguageConstruct::isLanguageConstruct($value)) {
|
||||
$reflector = new ReflectionLanguageConstruct($value);
|
||||
$doc = $this->getManualDocById($value);
|
||||
} else {
|
||||
list($target, $reflector) = $this->getTargetAndReflector($value, $output);
|
||||
$doc = $this->getManualDoc($reflector) ?: DocblockFormatter::format($reflector);
|
||||
}
|
||||
|
||||
$hasManual = $this->getShell()->getManual() !== null;
|
||||
|
||||
$shellOutput->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) && !$hasManual) {
|
||||
$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');
|
||||
} elseif ($doc !== null) {
|
||||
$output->writeln($doc);
|
||||
}
|
||||
|
||||
// Implicit --all if the original docblock has an {@inheritdoc} tag.
|
||||
if ($input->getOption('all') || ($doc && \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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$shellOutput->stopPaging();
|
||||
|
||||
// Set some magic local variables
|
||||
$this->setCommandScopeVariables($reflector);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the manual update operation.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @return int 0 if everything went fine, or an exit code
|
||||
*/
|
||||
private function handleUpdateManual(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
if (!$this->config) {
|
||||
$output->writeln('<error>Configuration not available for manual updates.</error>');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Create a synthetic input with the update-manual option
|
||||
$definition = new InputDefinition([
|
||||
new InputOption('update-manual', null, InputOption::VALUE_OPTIONAL, '', false),
|
||||
]);
|
||||
|
||||
// Get the language value: if true (no value), use null to preserve current language
|
||||
$lang = $input->getOption('update-manual');
|
||||
$updateValue = ($lang === true) ? null : $lang;
|
||||
|
||||
$updateInput = new ArrayInput(['--update-manual' => $updateValue], $definition);
|
||||
$updateInput->setInteractive($input->isInteractive());
|
||||
|
||||
try {
|
||||
$manualUpdate = ManualUpdate::fromConfig($this->config, $updateInput, $output);
|
||||
$result = $manualUpdate->run($updateInput, $output);
|
||||
|
||||
if ($result === 0) {
|
||||
$output->writeln('');
|
||||
$output->writeln('Restart PsySH to use the updated manual.');
|
||||
}
|
||||
|
||||
return $result;
|
||||
} catch (\RuntimeException $e) {
|
||||
$output->writeln(\sprintf('<error>%s</error>', $e->getMessage()));
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
// @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 ($manual = $this->getShell()->getManual()) {
|
||||
switch ($manual->getVersion()) {
|
||||
case 2:
|
||||
// v2 manual docs are pre-formatted and should be rendered as-is
|
||||
return $manual->get($id);
|
||||
|
||||
case 3:
|
||||
if ($doc = $manual->get($id)) {
|
||||
$width = Tty::getWidth();
|
||||
$formatter = new ManualFormatter($width, $manual);
|
||||
|
||||
return $formatter->format($doc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
84
vendor/psy/psysh/src/Command/DumpCommand.php
vendored
Normal file
84
vendor/psy/psysh/src/Command/DumpCommand.php
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use Psy\Input\CodeArgument;
|
||||
use Psy\VarDumper\Presenter;
|
||||
use Psy\VarDumper\PresenterAware;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Dump an object or primitive.
|
||||
*
|
||||
* This is like var_dump but *way* awesomer.
|
||||
*/
|
||||
class DumpCommand extends ReflectingCommand implements PresenterAware
|
||||
{
|
||||
private Presenter $presenter;
|
||||
|
||||
/**
|
||||
* PresenterAware interface.
|
||||
*
|
||||
* @param Presenter $presenter
|
||||
*/
|
||||
public function setPresenter(Presenter $presenter)
|
||||
{
|
||||
$this->presenter = $presenter;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('dump')
|
||||
->setDefinition([
|
||||
new CodeArgument('target', CodeArgument::REQUIRED, 'A target object or primitive to dump.'),
|
||||
new InputOption('depth', '', InputOption::VALUE_REQUIRED, 'Depth to parse.', 10),
|
||||
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Include private and protected methods and properties.'),
|
||||
])
|
||||
->setDescription('Dump an object or primitive.')
|
||||
->setHelp(
|
||||
<<<'HELP'
|
||||
Dump an object or primitive.
|
||||
|
||||
This is like var_dump but <strong>way</strong> awesomer.
|
||||
|
||||
e.g.
|
||||
<return>>>> dump $_</return>
|
||||
<return>>>> dump $someVar</return>
|
||||
<return>>>> dump $stuff->getAll()</return>
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int 0 if everything went fine, or an exit code
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$depth = $input->getOption('depth');
|
||||
$target = $this->resolveCode($input->getArgument('target'));
|
||||
$this->shellOutput($output)->page($this->presenter->present($target, $depth, ($input->getOption('all') ? Presenter::VERBOSE : 0) | Presenter::RAW), OutputInterface::OUTPUT_RAW);
|
||||
|
||||
if (\is_object($target)) {
|
||||
$this->setCommandScopeVariables(new \ReflectionObject($target));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
216
vendor/psy/psysh/src/Command/EditCommand.php
vendored
Normal file
216
vendor/psy/psysh/src/Command/EditCommand.php
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\ConfigPaths;
|
||||
use Psy\Context;
|
||||
use Psy\ContextAware;
|
||||
use Psy\Util\Tty;
|
||||
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
|
||||
{
|
||||
private string $runtimeDir = '';
|
||||
private Context $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(): void
|
||||
{
|
||||
$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): int
|
||||
{
|
||||
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) {
|
||||
ConfigPaths::ensureDir($this->runtimeDir);
|
||||
$filePath = \tempnam($this->runtimeDir, 'psysh-edit-command');
|
||||
$shouldRemoveFile = true;
|
||||
}
|
||||
|
||||
$editedContent = $this->editFile($filePath, $shouldRemoveFile);
|
||||
|
||||
if ($execute) {
|
||||
$this->getShell()->addInput($editedContent);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $execOption
|
||||
* @param bool $noExecOption
|
||||
* @param string|null $filePath
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @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';
|
||||
|
||||
// Enable signal characters so Ctrl-C can interrupt the editor.
|
||||
// PsySH's interactive readline disables isig at the prompt, but
|
||||
// the editor needs it to handle signals properly.
|
||||
$originalStty = null;
|
||||
if (Tty::supportsStty()) {
|
||||
$originalStty = \trim((string) @\shell_exec('stty -g 2>/dev/null'));
|
||||
@\shell_exec('stty isig 2>/dev/null');
|
||||
}
|
||||
|
||||
$pipes = [];
|
||||
$proc = \proc_open("{$editor} {$escapedFilePath}", [\STDIN, \STDOUT, \STDERR], $pipes);
|
||||
|
||||
// Ignore SIGINT in PsySH while the editor is running. The editor
|
||||
// handles ctrl-c itself; we just need to not die when the signal
|
||||
// is delivered to our process group. Set this after proc_open so
|
||||
// the editor inherits default signal handling.
|
||||
if (\function_exists('pcntl_signal')) {
|
||||
\pcntl_signal(\SIGINT, \SIG_IGN);
|
||||
}
|
||||
|
||||
try {
|
||||
\proc_close($proc);
|
||||
} finally {
|
||||
if (\function_exists('pcntl_signal')) {
|
||||
\pcntl_signal(\SIGINT, \SIG_DFL);
|
||||
}
|
||||
|
||||
if ($originalStty === null) {
|
||||
// nothing to restore
|
||||
} elseif ($originalStty === '') {
|
||||
@\shell_exec('stty -isig 2>/dev/null');
|
||||
} else {
|
||||
@\shell_exec('stty '.\escapeshellarg($originalStty).' 2>/dev/null');
|
||||
}
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
54
vendor/psy/psysh/src/Command/ExitCommand.php
vendored
Normal file
54
vendor/psy/psysh/src/Command/ExitCommand.php
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use Psy\Exception\BreakException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Exit the Psy Shell.
|
||||
*
|
||||
* Just what it says on the tin.
|
||||
*/
|
||||
class ExitCommand extends Command
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('exit')
|
||||
->setAliases(['quit', 'q'])
|
||||
->setDefinition([])
|
||||
->setDescription('End the current session and return to caller.')
|
||||
->setHelp(
|
||||
<<<'HELP'
|
||||
End the current session and return to caller.
|
||||
|
||||
e.g.
|
||||
<return>>>> exit</return>
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int 0 if everything went fine, or an exit code
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
throw new BreakException('Goodbye');
|
||||
}
|
||||
}
|
||||
188
vendor/psy/psysh/src/Command/HelpCommand.php
vendored
Normal file
188
vendor/psy/psysh/src/Command/HelpCommand.php
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\ManualWrapper;
|
||||
use Psy\Readline\Interactive\Layout\DisplayString;
|
||||
use Psy\Util\Tty;
|
||||
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Help command.
|
||||
*
|
||||
* Lists available commands, and gives command-specific help when asked nicely.
|
||||
*/
|
||||
class HelpCommand extends Command
|
||||
{
|
||||
private const TABLE_OVERHEAD_TWO_COLUMNS = 7;
|
||||
private const TABLE_OVERHEAD_THREE_COLUMNS = 10;
|
||||
private const MIN_DESCRIPTION_WIDTH_FOR_ALIAS_COLUMN = 40;
|
||||
|
||||
private ?Command $command = null;
|
||||
private ?InputInterface $commandInput = null;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('help')
|
||||
->setAliases(['?'])
|
||||
->setDefinition([
|
||||
new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name.', null),
|
||||
])
|
||||
->setDescription('Show a list of commands. Type `help [foo]` for information about [foo].')
|
||||
->setHelp('My. How meta.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for setting a subcommand to retrieve help for.
|
||||
*
|
||||
* @param Command $command
|
||||
*/
|
||||
public function setCommand(Command $command)
|
||||
{
|
||||
$this->command = $command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for preserving the original input when rendering contextual help.
|
||||
*/
|
||||
public function setCommandInput(InputInterface $input): void
|
||||
{
|
||||
$this->commandInput = $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int 0 if everything went fine, or an exit code
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$shellOutput = $this->shellOutput($output);
|
||||
|
||||
if ($this->command !== null) {
|
||||
// help for an individual command
|
||||
$shellOutput->page($this->command->asTextForInput($this->commandInput ?? $input));
|
||||
$this->command = null;
|
||||
$this->commandInput = null;
|
||||
} elseif ($name = $input->getArgument('command_name')) {
|
||||
// help for an individual command
|
||||
try {
|
||||
$cmd = $this->getApplication()->get($name);
|
||||
} catch (CommandNotFoundException $e) {
|
||||
$this->getShell()->writeException($e);
|
||||
$output->writeln('');
|
||||
$output->writeln(\sprintf(
|
||||
'<aside>To read PHP documentation, use <return>doc %s</return></aside>',
|
||||
$name
|
||||
));
|
||||
$output->writeln('');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!$cmd instanceof Command) {
|
||||
throw new \RuntimeException(\sprintf('Expected Psy\Command\Command instance, got %s', \get_class($cmd)));
|
||||
}
|
||||
|
||||
$shellOutput->page($cmd->asTextForInput($input));
|
||||
} else {
|
||||
$this->commandInput = null;
|
||||
$shellOutput->page(function (OutputInterface $pagedOutput): void {
|
||||
$this->renderCommandList($pagedOutput);
|
||||
});
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the top-level command list with fixed command widths and a
|
||||
* conditional alias column when the terminal is wide enough.
|
||||
*/
|
||||
private function renderCommandList(OutputInterface $output): void
|
||||
{
|
||||
$commands = [];
|
||||
|
||||
foreach ($this->getApplication()->all() as $name => $command) {
|
||||
if ($name !== $command->getName()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$commands[] = [
|
||||
'name' => $name,
|
||||
'description' => $command->getDescription(),
|
||||
'aliasText' => $command->getAliases()
|
||||
? \sprintf('<comment>Aliases:</comment> %s', \implode(', ', $command->getAliases()))
|
||||
: '',
|
||||
];
|
||||
}
|
||||
|
||||
$nameWidth = 0;
|
||||
$aliasWidth = 0;
|
||||
$descriptionWidth = 0;
|
||||
$formatter = $output->getFormatter();
|
||||
foreach ($commands as $command) {
|
||||
$nameWidth = \max($nameWidth, DisplayString::width($command['name']));
|
||||
$aliasWidth = \max($aliasWidth, DisplayString::widthWithoutFormatting($command['aliasText'], $formatter));
|
||||
$descriptionWidth = \max($descriptionWidth, DisplayString::width($command['description']));
|
||||
}
|
||||
|
||||
$terminalWidth = Tty::getWidth();
|
||||
$wrapper = new ManualWrapper();
|
||||
$table = $this->getTable($output)->setColumnWidth(0, $nameWidth);
|
||||
$descriptionWidthWithAliasColumn = $terminalWidth - $nameWidth - $aliasWidth - self::TABLE_OVERHEAD_THREE_COLUMNS;
|
||||
|
||||
if ($aliasWidth > 0 && $descriptionWidthWithAliasColumn >= self::MIN_DESCRIPTION_WIDTH_FOR_ALIAS_COLUMN) {
|
||||
$descriptionColumnWidth = \min($descriptionWidth, $descriptionWidthWithAliasColumn);
|
||||
|
||||
$table
|
||||
->setColumnWidth(1, $descriptionColumnWidth)
|
||||
->setColumnWidth(2, $aliasWidth);
|
||||
|
||||
foreach ($commands as $command) {
|
||||
$table->addRow([
|
||||
\sprintf('<info>%s</info>', $command['name']),
|
||||
$wrapper->wrap($command['description'], $descriptionColumnWidth),
|
||||
$command['aliasText'],
|
||||
]);
|
||||
}
|
||||
|
||||
$table->render();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$detailsWidth = \max(10, $terminalWidth - $nameWidth - self::TABLE_OVERHEAD_TWO_COLUMNS);
|
||||
$table->setColumnWidth(1, $detailsWidth);
|
||||
|
||||
foreach ($commands as $command) {
|
||||
$details = $wrapper->wrap($command['description'], $detailsWidth);
|
||||
if ($command['aliasText'] !== '') {
|
||||
$details .= "\n".$wrapper->wrap($command['aliasText'], $detailsWidth);
|
||||
}
|
||||
|
||||
$table->addRow([
|
||||
\sprintf('<info>%s</info>', $command['name']),
|
||||
$details,
|
||||
]);
|
||||
}
|
||||
|
||||
$table->render();
|
||||
}
|
||||
}
|
||||
270
vendor/psy/psysh/src/Command/HistoryCommand.php
vendored
Normal file
270
vendor/psy/psysh/src/Command/HistoryCommand.php
vendored
Normal file
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\ConfigPaths;
|
||||
use Psy\Input\FilterOptions;
|
||||
use Psy\Output\ShellOutputAdapter;
|
||||
use Psy\Readline\Readline;
|
||||
use Psy\Readline\ReadlineAware;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Psy Shell history command.
|
||||
*
|
||||
* Shows, searches and replays readline history. Not too shabby.
|
||||
*/
|
||||
class HistoryCommand extends Command implements ReadlineAware
|
||||
{
|
||||
private FilterOptions $filter;
|
||||
private Readline $readline;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($name = null)
|
||||
{
|
||||
$this->filter = new FilterOptions();
|
||||
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Shell's Readline service.
|
||||
*
|
||||
* @param Readline $readline
|
||||
*/
|
||||
public function setReadline(Readline $readline)
|
||||
{
|
||||
$this->readline = $readline;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
list($grep, $insensitive, $invert) = FilterOptions::getOptions();
|
||||
|
||||
$this
|
||||
->setName('history')
|
||||
->setAliases(['hist'])
|
||||
->setDefinition([
|
||||
new InputOption('show', 's', InputOption::VALUE_REQUIRED, 'Show the given range of lines.'),
|
||||
new InputOption('head', 'H', InputOption::VALUE_REQUIRED, 'Display the first N items.'),
|
||||
new InputOption('tail', 'T', InputOption::VALUE_REQUIRED, 'Display the last N items.'),
|
||||
|
||||
$grep,
|
||||
$insensitive,
|
||||
$invert,
|
||||
|
||||
new InputOption('no-numbers', 'N', InputOption::VALUE_NONE, 'Omit line numbers.'),
|
||||
|
||||
new InputOption('save', '', InputOption::VALUE_REQUIRED, 'Save history to a file.'),
|
||||
new InputOption('replay', '', InputOption::VALUE_NONE, 'Replay.'),
|
||||
new InputOption('clear', '', InputOption::VALUE_NONE, 'Clear the history.'),
|
||||
])
|
||||
->setDescription('Show the Psy Shell history.')
|
||||
->setHelp(
|
||||
<<<'HELP'
|
||||
Show, search, save or replay the Psy Shell history.
|
||||
|
||||
e.g.
|
||||
<return>>>> history --grep /[bB]acon/</return>
|
||||
<return>>>> history --show 0..10 --replay</return>
|
||||
<return>>>> history --clear</return>
|
||||
<return>>>> history --tail 1000 --save somefile.txt</return>
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int 0 if everything went fine, or an exit code
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->validateOnlyOne($input, ['show', 'head', 'tail']);
|
||||
$this->validateOnlyOne($input, ['save', 'replay', 'clear']);
|
||||
|
||||
// For --show, slice first (uses original line numbers), then filter
|
||||
$show = $input->getOption('show');
|
||||
|
||||
// For --head/--tail, filter first, then slice (uses result count)
|
||||
$head = $input->getOption('head');
|
||||
$tail = $input->getOption('tail');
|
||||
|
||||
$history = $this->getHistorySlice($show);
|
||||
$highlighted = false;
|
||||
|
||||
$this->filter->bind($input);
|
||||
if ($this->filter->hasFilter()) {
|
||||
$matches = [];
|
||||
$highlighted = [];
|
||||
foreach ($history as $i => $line) {
|
||||
if ($this->filter->match($line, $matches)) {
|
||||
if (isset($matches[0])) {
|
||||
$chunks = \explode($matches[0], $history[$i]);
|
||||
$chunks = \array_map([__CLASS__, 'escape'], $chunks);
|
||||
$glue = \sprintf('<urgent>%s</urgent>', self::escape($matches[0]));
|
||||
|
||||
$highlighted[$i] = \implode($glue, $chunks);
|
||||
}
|
||||
} else {
|
||||
unset($history[$i]);
|
||||
unset($highlighted[$i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$history = $this->applyHeadOrTail($history, $head, $tail);
|
||||
if ($highlighted) {
|
||||
$highlighted = $this->applyHeadOrTail($highlighted, $head, $tail);
|
||||
}
|
||||
|
||||
if ($save = $input->getOption('save')) {
|
||||
$output->writeln(\sprintf('Saving history in %s...', ConfigPaths::prettyPath($save)));
|
||||
\file_put_contents($save, \implode(\PHP_EOL, $history).\PHP_EOL);
|
||||
$output->writeln('<info>History saved.</info>');
|
||||
} elseif ($input->getOption('replay')) {
|
||||
if (!($input->getOption('show') || $input->getOption('head') || $input->getOption('tail'))) {
|
||||
throw new \InvalidArgumentException('You must limit history via --head, --tail or --show before replaying');
|
||||
}
|
||||
|
||||
$count = \count($history);
|
||||
$output->writeln(\sprintf('Replaying %d line%s of history', $count, ($count !== 1) ? 's' : ''));
|
||||
|
||||
$this->getShell()->addInput($history);
|
||||
} elseif ($input->getOption('clear')) {
|
||||
$this->clearHistory();
|
||||
$output->writeln('<info>History cleared.</info>');
|
||||
} else {
|
||||
$type = $input->getOption('no-numbers') ? 0 : ShellOutputAdapter::NUMBER_LINES;
|
||||
if (!$highlighted) {
|
||||
$type = $type | OutputInterface::OUTPUT_RAW;
|
||||
}
|
||||
|
||||
$this->shellOutput($output)->page($highlighted ?: $history, $type);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a range from a string.
|
||||
*
|
||||
* @param string $range
|
||||
*
|
||||
* @return int[] [ start, end ]
|
||||
*/
|
||||
private function extractRange(string $range): array
|
||||
{
|
||||
if (\preg_match('/^\d+$/', $range)) {
|
||||
return [(int) $range, (int) $range + 1];
|
||||
}
|
||||
|
||||
$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 [$start, $end];
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Unexpected range: '.$range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a slice of the readline history by range.
|
||||
*
|
||||
* @param string|null $show Range specification (e.g., "5..10")
|
||||
*
|
||||
* @return array A slice of history
|
||||
*/
|
||||
private function getHistorySlice(?string $show): array
|
||||
{
|
||||
$history = $this->readline->listHistory();
|
||||
// don't show the current `history` invocation
|
||||
\array_pop($history);
|
||||
|
||||
if ($show === null) {
|
||||
return $history;
|
||||
}
|
||||
|
||||
list($start, $end) = $this->extractRange($show);
|
||||
$length = $end - $start;
|
||||
|
||||
return \array_slice($history, $start, $length, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply --head or --tail to a history array.
|
||||
*/
|
||||
private function applyHeadOrTail(array $history, ?string $head, ?string $tail): array
|
||||
{
|
||||
if ($head) {
|
||||
if (!\preg_match('/^\d+$/', $head)) {
|
||||
throw new \InvalidArgumentException('Please specify an integer argument for --head');
|
||||
}
|
||||
|
||||
return \array_slice($history, 0, (int) $head, true);
|
||||
} elseif ($tail) {
|
||||
if (!\preg_match('/^\d+$/', $tail)) {
|
||||
throw new \InvalidArgumentException('Please specify an integer argument for --tail');
|
||||
}
|
||||
|
||||
$start = \count($history) - (int) $tail;
|
||||
$length = (int) $tail + 1;
|
||||
|
||||
return \array_slice($history, $start, $length, true);
|
||||
}
|
||||
|
||||
return $history;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that only one of the given $options is set.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param array $options
|
||||
*/
|
||||
private function validateOnlyOne(InputInterface $input, array $options)
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($options as $opt) {
|
||||
if ($input->getOption($opt)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($count > 1) {
|
||||
throw new \InvalidArgumentException('Please specify only one of --'.\implode(', --', $options));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the readline history.
|
||||
*/
|
||||
private function clearHistory()
|
||||
{
|
||||
$this->readline->clearHistory();
|
||||
}
|
||||
|
||||
public static function escape(string $string): string
|
||||
{
|
||||
return OutputFormatter::escape($string);
|
||||
}
|
||||
}
|
||||
287
vendor/psy/psysh/src/Command/ListCommand.php
vendored
Normal file
287
vendor/psy/psysh/src/Command/ListCommand.php
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use Psy\Command\ListCommand\ClassConstantEnumerator;
|
||||
use Psy\Command\ListCommand\ClassEnumerator;
|
||||
use Psy\Command\ListCommand\ConstantEnumerator;
|
||||
use Psy\Command\ListCommand\FunctionEnumerator;
|
||||
use Psy\Command\ListCommand\GlobalVariableEnumerator;
|
||||
use Psy\Command\ListCommand\MethodEnumerator;
|
||||
use Psy\Command\ListCommand\PropertyEnumerator;
|
||||
use Psy\Command\ListCommand\VariableEnumerator;
|
||||
use Psy\Exception\RuntimeException;
|
||||
use Psy\Input\CodeArgument;
|
||||
use Psy\Input\FilterOptions;
|
||||
use Psy\VarDumper\Presenter;
|
||||
use Psy\VarDumper\PresenterAware;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* List available local variables, object properties, etc.
|
||||
*/
|
||||
class ListCommand extends ReflectingCommand implements PresenterAware
|
||||
{
|
||||
protected Presenter $presenter;
|
||||
protected array $enumerators;
|
||||
|
||||
/**
|
||||
* PresenterAware interface.
|
||||
*
|
||||
* @param Presenter $presenter
|
||||
*/
|
||||
public function setPresenter(Presenter $presenter)
|
||||
{
|
||||
$this->presenter = $presenter;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
list($grep, $insensitive, $invert) = FilterOptions::getOptions();
|
||||
|
||||
$this
|
||||
->setName('ls')
|
||||
->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('no-inherit', '', InputOption::VALUE_NONE, 'Exclude inherited methods, properties and constants.'),
|
||||
|
||||
new InputOption('properties', 'p', InputOption::VALUE_NONE, 'Display class or object properties (public properties by default).'),
|
||||
new InputOption('methods', 'm', InputOption::VALUE_NONE, 'Display class or object methods (public methods by default).'),
|
||||
|
||||
$grep,
|
||||
$insensitive,
|
||||
$invert,
|
||||
|
||||
new InputOption('globals', 'g', InputOption::VALUE_NONE, 'Include global variables.'),
|
||||
new InputOption('internal', 'n', InputOption::VALUE_NONE, 'Limit to internal functions and classes.'),
|
||||
new InputOption('user', 'u', InputOption::VALUE_NONE, 'Limit to user-defined constants, functions and classes.'),
|
||||
new InputOption('category', 'C', InputOption::VALUE_REQUIRED, 'Limit to constants in a specific category (e.g. "date").'),
|
||||
|
||||
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Include private and protected methods and properties.'),
|
||||
new InputOption('long', 'l', InputOption::VALUE_NONE, 'List in long format: includes class names and method signatures.'),
|
||||
])
|
||||
->setDescription('List local, instance or class variables, methods and constants.')
|
||||
->setHelp(
|
||||
<<<'HELP'
|
||||
List variables, constants, classes, interfaces, traits, functions, methods,
|
||||
and properties.
|
||||
|
||||
Called without options, this will return a list of variables currently in scope.
|
||||
|
||||
If a target object is provided, list properties, constants and methods of that
|
||||
target. If a class, interface or trait name is passed instead, list constants
|
||||
and methods on that class.
|
||||
|
||||
e.g.
|
||||
<return>>>> ls</return>
|
||||
<return>>>> ls $foo</return>
|
||||
<return>>>> ls -k --grep mongo -i</return>
|
||||
<return>>>> ls -al ReflectionClass</return>
|
||||
<return>>>> ls --constants --category date</return>
|
||||
<return>>>> ls -l --functions --grep /^array_.*/</return>
|
||||
<return>>>> ls -l --properties new DateTime()</return>
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int 0 if everything went fine, or an exit code
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->validateInput($input);
|
||||
$this->initEnumerators();
|
||||
$shellOutput = $this->shellOutput($output);
|
||||
|
||||
$method = $input->getOption('long') ? 'writeLong' : 'write';
|
||||
|
||||
if ($target = $input->getArgument('target')) {
|
||||
list($target, $reflector) = $this->getTargetAndReflector($target, $output);
|
||||
} else {
|
||||
$reflector = null;
|
||||
}
|
||||
|
||||
if ($input->getOption('long')) {
|
||||
$shellOutput->startPaging();
|
||||
}
|
||||
|
||||
foreach ($this->enumerators as $enumerator) {
|
||||
$this->$method($output, $enumerator->enumerate($input, $reflector, $target));
|
||||
}
|
||||
|
||||
if ($input->getOption('long')) {
|
||||
$shellOutput->stopPaging();
|
||||
}
|
||||
|
||||
// Set some magic local variables
|
||||
if ($reflector !== null) {
|
||||
$this->setCommandScopeVariables($reflector);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Enumerators.
|
||||
*/
|
||||
protected function initEnumerators()
|
||||
{
|
||||
if (!isset($this->enumerators)) {
|
||||
$mgr = $this->presenter;
|
||||
|
||||
$this->enumerators = [
|
||||
new ClassConstantEnumerator($mgr),
|
||||
new ClassEnumerator($mgr),
|
||||
new ConstantEnumerator($mgr),
|
||||
new FunctionEnumerator($mgr),
|
||||
new GlobalVariableEnumerator($mgr),
|
||||
new PropertyEnumerator($mgr),
|
||||
new MethodEnumerator($mgr),
|
||||
new VariableEnumerator($mgr, $this->context),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the list items to $output.
|
||||
*
|
||||
* @param OutputInterface $output
|
||||
* @param array $result List of enumerated items
|
||||
*/
|
||||
protected function write(OutputInterface $output, array $result)
|
||||
{
|
||||
if (\count($result) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$formatter = $output->getFormatter();
|
||||
|
||||
foreach ($result as $label => $items) {
|
||||
// Pre-format each item individually to avoid O(n^2) performance
|
||||
// in Symfony's OutputFormatter when processing large strings with many style tags.
|
||||
$names = \array_map(fn ($item) => $formatter->format($this->formatItemName($item)), $items);
|
||||
|
||||
// Pre-format the label and join with pre-formatted names
|
||||
$line = $formatter->format(\sprintf('<strong>%s</strong>: ', $label)).\implode(', ', $names);
|
||||
|
||||
// Write raw since we've already formatted everything
|
||||
$output->writeln($line, OutputInterface::OUTPUT_RAW);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the list items to $output.
|
||||
*
|
||||
* Items are listed one per line, and include the item signature.
|
||||
*
|
||||
* @param OutputInterface $output
|
||||
* @param array $result List of enumerated items
|
||||
*/
|
||||
protected function writeLong(OutputInterface $output, array $result)
|
||||
{
|
||||
if (\count($result) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$table = $this->getTable($output);
|
||||
$first = true;
|
||||
|
||||
foreach ($result as $label => $items) {
|
||||
if (!$first) {
|
||||
$output->writeln('');
|
||||
}
|
||||
$output->writeln(\sprintf('<strong>%s:</strong>', $label));
|
||||
|
||||
$table->setRows([]);
|
||||
foreach ($items as $item) {
|
||||
$table->addRow([$this->formatItemName($item), $item['value']]);
|
||||
}
|
||||
|
||||
$table->render();
|
||||
$first = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an item name given its visibility.
|
||||
*
|
||||
* @param array $item
|
||||
*/
|
||||
private function formatItemName(array $item): string
|
||||
{
|
||||
return \sprintf('<%s>%s</%s>', $item['style'], OutputFormatter::escape($item['name']), $item['style']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that input options make sense, provide defaults when called without options.
|
||||
*
|
||||
* @throws RuntimeException if options are inconsistent
|
||||
*
|
||||
* @param InputInterface $input
|
||||
*/
|
||||
private function validateInput(InputInterface $input)
|
||||
{
|
||||
if (!$input->getArgument('target')) {
|
||||
// if no target is passed, there can be no properties or methods
|
||||
foreach (['properties', 'methods', 'no-inherit'] as $option) {
|
||||
if ($input->getOption($option)) {
|
||||
throw new RuntimeException('--'.$option.' does not make sense without a specified target');
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['globals', 'vars', 'constants', 'functions', 'classes', 'interfaces', 'traits'] as $option) {
|
||||
if ($input->getOption($option)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// default to --vars if no other options are passed
|
||||
$input->setOption('vars', true);
|
||||
} else {
|
||||
// if a target is passed, classes, functions, etc don't make sense
|
||||
foreach (['vars', 'globals'] as $option) {
|
||||
if ($input->getOption($option)) {
|
||||
throw new RuntimeException('--'.$option.' does not make sense with a specified target');
|
||||
}
|
||||
}
|
||||
|
||||
// @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('properties', true);
|
||||
$input->setOption('methods', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
121
vendor/psy/psysh/src/Command/ListCommand/ClassConstantEnumerator.php
vendored
Normal file
121
vendor/psy/psysh/src/Command/ListCommand/ClassConstantEnumerator.php
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command\ListCommand;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Class Constant Enumerator class.
|
||||
*/
|
||||
class ClassConstantEnumerator extends Enumerator
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function listItems(InputInterface $input, ?\Reflector $reflector = null, $target = null): array
|
||||
{
|
||||
// only list constants when a Reflector is present.
|
||||
if ($reflector === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// We can only list constants on actual class (or object) reflectors.
|
||||
if (!$reflector instanceof \ReflectionClass) {
|
||||
// @todo handle ReflectionExtension as well
|
||||
return [];
|
||||
}
|
||||
|
||||
// only list constants if we are specifically asked
|
||||
if (!$input->getOption('constants')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$noInherit = $input->getOption('no-inherit');
|
||||
$constants = $this->prepareConstants($this->getConstants($reflector, $noInherit));
|
||||
|
||||
if (empty($constants)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ret = [];
|
||||
$ret[$this->getKindLabel($reflector)] = $constants;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get defined constants for the given class or object Reflector.
|
||||
*
|
||||
* @param \ReflectionClass $reflector
|
||||
* @param bool $noInherit Exclude inherited constants
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getConstants(\ReflectionClass $reflector, bool $noInherit = false): array
|
||||
{
|
||||
$className = $reflector->getName();
|
||||
|
||||
$constants = [];
|
||||
foreach ($reflector->getConstants() as $name => $constant) {
|
||||
$constReflector = new \ReflectionClassConstant($reflector->name, $name);
|
||||
|
||||
if ($noInherit && $constReflector->getDeclaringClass()->getName() !== $className) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$constants[$name] = $constReflector;
|
||||
}
|
||||
|
||||
\ksort($constants, \SORT_NATURAL | \SORT_FLAG_CASE);
|
||||
|
||||
return $constants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare formatted constant array.
|
||||
*
|
||||
* @param array $constants
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareConstants(array $constants): array
|
||||
{
|
||||
// My kingdom for a generator.
|
||||
$ret = [];
|
||||
|
||||
foreach ($constants as $name => $constant) {
|
||||
if ($this->showItem($name)) {
|
||||
$ret[$name] = [
|
||||
'name' => $name,
|
||||
'style' => self::IS_CONSTANT,
|
||||
'value' => $this->presentRef($constant->getValue()),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a label for the particular kind of "class" represented.
|
||||
*
|
||||
* @param \ReflectionClass $reflector
|
||||
*/
|
||||
protected function getKindLabel(\ReflectionClass $reflector): string
|
||||
{
|
||||
if ($reflector->isInterface()) {
|
||||
return 'Interface Constants';
|
||||
} else {
|
||||
return 'Class Constants';
|
||||
}
|
||||
}
|
||||
}
|
||||
130
vendor/psy/psysh/src/Command/ListCommand/ClassEnumerator.php
vendored
Normal file
130
vendor/psy/psysh/src/Command/ListCommand/ClassEnumerator.php
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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|null $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, fn ($class) => $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;
|
||||
}
|
||||
}
|
||||
176
vendor/psy/psysh/src/Command/ListCommand/ConstantEnumerator.php
vendored
Normal file
176
vendor/psy/psysh/src/Command/ListCommand/ConstantEnumerator.php
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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 const CATEGORY_LABELS = [
|
||||
'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::CATEGORY_LABELS) ? self::CATEGORY_LABELS[$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|null $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']);
|
||||
$values = \array_values($consts);
|
||||
|
||||
return $values ? \array_merge(...$values) : [];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
114
vendor/psy/psysh/src/Command/ListCommand/Enumerator.php
vendored
Normal file
114
vendor/psy/psysh/src/Command/ListCommand/Enumerator.php
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command\ListCommand;
|
||||
|
||||
use Psy\Formatter\SignatureFormatter;
|
||||
use Psy\Input\FilterOptions;
|
||||
use Psy\Util\Mirror;
|
||||
use Psy\VarDumper\Presenter;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Abstract Enumerator class.
|
||||
*/
|
||||
abstract class Enumerator
|
||||
{
|
||||
// Output styles
|
||||
const IS_PUBLIC = 'public';
|
||||
const IS_PROTECTED = 'protected';
|
||||
const IS_PRIVATE = 'private';
|
||||
const IS_GLOBAL = 'global';
|
||||
const IS_CONSTANT = 'const';
|
||||
const IS_CLASS = 'class';
|
||||
const IS_FUNCTION = 'function';
|
||||
const IS_VIRTUAL = 'virtual';
|
||||
|
||||
private FilterOptions $filter;
|
||||
private Presenter $presenter;
|
||||
|
||||
/**
|
||||
* Enumerator constructor.
|
||||
*
|
||||
* @param Presenter $presenter
|
||||
*/
|
||||
public function __construct(Presenter $presenter)
|
||||
{
|
||||
$this->filter = new FilterOptions();
|
||||
$this->presenter = $presenter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of categorized things with the given input options and target.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param \Reflector|null $reflector
|
||||
* @param mixed $target
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function enumerate(InputInterface $input, ?\Reflector $reflector = null, $target = null): array
|
||||
{
|
||||
$this->filter->bind($input);
|
||||
|
||||
return $this->listItems($input, $reflector, $target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerate specific items with the given input options and target.
|
||||
*
|
||||
* Implementing classes should return an array of arrays:
|
||||
*
|
||||
* [
|
||||
* 'Constants' => [
|
||||
* 'FOO' => [
|
||||
* 'name' => 'FOO',
|
||||
* 'style' => 'public',
|
||||
* 'value' => '123',
|
||||
* ],
|
||||
* ],
|
||||
* ]
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param \Reflector|null $reflector
|
||||
* @param mixed $target
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function listItems(InputInterface $input, ?\Reflector $reflector = null, $target = null): array;
|
||||
|
||||
protected function showItem($name)
|
||||
{
|
||||
return $this->filter->match($name);
|
||||
}
|
||||
|
||||
protected function presentRef($value)
|
||||
{
|
||||
// Symfony VarDumper 5.4 trips over NAN/INF on PHP 8.5 in PHAR builds,
|
||||
// so format non-finite floats directly instead of cloning them.
|
||||
if (\is_float($value) && !\is_finite($value)) {
|
||||
return OutputFormatter::escape(\sprintf('<float>%s</float>', \var_export($value, true)));
|
||||
}
|
||||
|
||||
return $this->presenter->presentRef($value);
|
||||
}
|
||||
|
||||
protected function presentSignature($target)
|
||||
{
|
||||
// This might get weird if the signature is actually for a reflector. Hrm.
|
||||
if (!$target instanceof \Reflector) {
|
||||
$target = Mirror::get($target);
|
||||
}
|
||||
|
||||
return SignatureFormatter::format($target);
|
||||
}
|
||||
}
|
||||
116
vendor/psy/psysh/src/Command/ListCommand/FunctionEnumerator.php
vendored
Normal file
116
vendor/psy/psysh/src/Command/ListCommand/FunctionEnumerator.php
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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|null $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;
|
||||
}
|
||||
}
|
||||
92
vendor/psy/psysh/src/Command/ListCommand/GlobalVariableEnumerator.php
vendored
Normal file
92
vendor/psy/psysh/src/Command/ListCommand/GlobalVariableEnumerator.php
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command\ListCommand;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Global Variable Enumerator class.
|
||||
*/
|
||||
class GlobalVariableEnumerator extends Enumerator
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function listItems(InputInterface $input, ?\Reflector $reflector = null, $target = null): array
|
||||
{
|
||||
// only list globals when no Reflector is present.
|
||||
if ($reflector !== null || $target !== null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// only list globals if we are specifically asked
|
||||
if (!$input->getOption('globals')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$globals = $this->prepareGlobals($this->getGlobals());
|
||||
|
||||
if (empty($globals)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'Global Variables' => $globals,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get defined global variables.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getGlobals(): array
|
||||
{
|
||||
global $GLOBALS;
|
||||
|
||||
$names = \array_keys($GLOBALS);
|
||||
\natcasesort($names);
|
||||
|
||||
$ret = [];
|
||||
foreach ($names as $name) {
|
||||
$ret[$name] = $GLOBALS[$name];
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare formatted global variable array.
|
||||
*
|
||||
* @param array $globals
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareGlobals(array $globals): array
|
||||
{
|
||||
// My kingdom for a generator.
|
||||
$ret = [];
|
||||
|
||||
foreach ($globals as $name => $value) {
|
||||
if ($this->showItem($name)) {
|
||||
$fname = '$'.$name;
|
||||
$ret[$fname] = [
|
||||
'name' => $fname,
|
||||
'style' => self::IS_GLOBAL,
|
||||
'value' => $this->presentRef($value),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
160
vendor/psy/psysh/src/Command/ListCommand/MethodEnumerator.php
vendored
Normal file
160
vendor/psy/psysh/src/Command/ListCommand/MethodEnumerator.php
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\ReflectionMagicMethod;
|
||||
use Psy\Util\Docblock;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Method Enumerator class.
|
||||
*/
|
||||
class MethodEnumerator extends Enumerator
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function listItems(InputInterface $input, ?\Reflector $reflector = null, $target = null): array
|
||||
{
|
||||
// only list methods when a Reflector is present.
|
||||
if ($reflector === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// We can only list methods on actual class (or object) reflectors.
|
||||
if (!$reflector instanceof \ReflectionClass) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// only list methods if we are specifically asked
|
||||
if (!$input->getOption('methods')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$showAll = $input->getOption('all');
|
||||
$noInherit = $input->getOption('no-inherit');
|
||||
$methods = $this->prepareMethods($this->getMethods($showAll, $reflector, $noInherit));
|
||||
|
||||
if (empty($methods)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ret = [];
|
||||
$ret[$this->getKindLabel($reflector)] = $methods;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get defined methods for the given class or object Reflector.
|
||||
*
|
||||
* @param bool $showAll Include private and protected methods
|
||||
* @param \ReflectionClass $reflector
|
||||
* @param bool $noInherit Exclude inherited methods
|
||||
*
|
||||
* @return \ReflectionMethod[]
|
||||
*/
|
||||
protected function getMethods(bool $showAll, \ReflectionClass $reflector, bool $noInherit = false): array
|
||||
{
|
||||
$className = $reflector->getName();
|
||||
|
||||
$methods = [];
|
||||
foreach ($reflector->getMethods() as $name => $method) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
if ($showAll || $method->isPublic()) {
|
||||
$methods[$method->getName()] = $method;
|
||||
}
|
||||
}
|
||||
|
||||
// Add magic methods from docblock @method tags
|
||||
foreach (Docblock::getMagicMethods($reflector) as $method) {
|
||||
if ($noInherit && $method->getDeclaringClass()->getName() !== $className) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if a real method with this name already exists
|
||||
if (!isset($methods[$method->getName()])) {
|
||||
$methods[$method->getName()] = $method;
|
||||
}
|
||||
}
|
||||
|
||||
\ksort($methods, \SORT_NATURAL | \SORT_FLAG_CASE);
|
||||
|
||||
return $methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare formatted method array.
|
||||
*
|
||||
* @param \ReflectionMethod[] $methods
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareMethods(array $methods): array
|
||||
{
|
||||
// My kingdom for a generator.
|
||||
$ret = [];
|
||||
|
||||
foreach ($methods as $name => $method) {
|
||||
if ($this->showItem($name)) {
|
||||
$ret[$name] = [
|
||||
'name' => $name,
|
||||
'style' => $this->getVisibilityStyle($method),
|
||||
'value' => $this->presentSignature($method),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a label for the particular kind of "class" represented.
|
||||
*
|
||||
* @param \ReflectionClass $reflector
|
||||
*/
|
||||
protected function getKindLabel(\ReflectionClass $reflector): string
|
||||
{
|
||||
if ($reflector->isInterface()) {
|
||||
return 'Interface Methods';
|
||||
} elseif (\method_exists($reflector, 'isTrait') && $reflector->isTrait()) {
|
||||
return 'Trait Methods';
|
||||
} else {
|
||||
return 'Class Methods';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output style for the given method's visibility.
|
||||
*
|
||||
* @param \ReflectionMethod|ReflectionMagicMethod $method
|
||||
*/
|
||||
private function getVisibilityStyle(\Reflector $method): string
|
||||
{
|
||||
if ($method instanceof ReflectionMagicMethod) {
|
||||
return self::IS_VIRTUAL;
|
||||
}
|
||||
|
||||
if ($method->isPublic()) {
|
||||
return self::IS_PUBLIC;
|
||||
} elseif ($method->isProtected()) {
|
||||
return self::IS_PROTECTED;
|
||||
} else {
|
||||
return self::IS_PRIVATE;
|
||||
}
|
||||
}
|
||||
}
|
||||
201
vendor/psy/psysh/src/Command/ListCommand/PropertyEnumerator.php
vendored
Normal file
201
vendor/psy/psysh/src/Command/ListCommand/PropertyEnumerator.php
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\ReflectionMagicProperty;
|
||||
use Psy\Util\Docblock;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Property Enumerator class.
|
||||
*/
|
||||
class PropertyEnumerator extends Enumerator
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function listItems(InputInterface $input, ?\Reflector $reflector = null, $target = null): array
|
||||
{
|
||||
// only list properties when a Reflector is present.
|
||||
|
||||
if ($reflector === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// We can only list properties on actual class (or object) reflectors.
|
||||
if (!$reflector instanceof \ReflectionClass) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// only list properties if we are specifically asked
|
||||
if (!$input->getOption('properties')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$showAll = $input->getOption('all');
|
||||
$noInherit = $input->getOption('no-inherit');
|
||||
$properties = $this->prepareProperties($this->getProperties($showAll, $reflector, $noInherit), $target);
|
||||
|
||||
if (empty($properties)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ret = [];
|
||||
$ret[$this->getKindLabel($reflector)] = $properties;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get defined properties for the given class or object Reflector.
|
||||
*
|
||||
* @param bool $showAll Include private and protected properties
|
||||
* @param \ReflectionClass $reflector
|
||||
* @param bool $noInherit Exclude inherited properties
|
||||
*
|
||||
* @return \ReflectionProperty[]
|
||||
*/
|
||||
protected function getProperties(bool $showAll, \ReflectionClass $reflector, bool $noInherit = false): array
|
||||
{
|
||||
$className = $reflector->getName();
|
||||
|
||||
$properties = [];
|
||||
foreach ($reflector->getProperties() as $property) {
|
||||
if ($noInherit && $property->getDeclaringClass()->getName() !== $className) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($showAll || $property->isPublic()) {
|
||||
$properties[$property->getName()] = $property;
|
||||
}
|
||||
}
|
||||
|
||||
// Add magic properties from docblock @property tags
|
||||
foreach (Docblock::getMagicProperties($reflector) as $property) {
|
||||
if ($noInherit && $property->getDeclaringClass()->getName() !== $className) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if a real property with this name already exists
|
||||
if (!isset($properties[$property->getName()])) {
|
||||
$properties[$property->getName()] = $property;
|
||||
}
|
||||
}
|
||||
|
||||
\ksort($properties, \SORT_NATURAL | \SORT_FLAG_CASE);
|
||||
|
||||
return $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare formatted property array.
|
||||
*
|
||||
* @param \ReflectionProperty[] $properties
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareProperties(array $properties, $target = null): array
|
||||
{
|
||||
// My kingdom for a generator.
|
||||
$ret = [];
|
||||
|
||||
foreach ($properties as $name => $property) {
|
||||
if ($this->showItem($name)) {
|
||||
$fname = '$'.$name;
|
||||
$ret[$fname] = [
|
||||
'name' => $fname,
|
||||
'style' => $this->getVisibilityStyle($property),
|
||||
'value' => $this->presentValue($property, $target),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a label for the particular kind of "class" represented.
|
||||
*
|
||||
* @param \ReflectionClass $reflector
|
||||
*/
|
||||
protected function getKindLabel(\ReflectionClass $reflector): string
|
||||
{
|
||||
if (\method_exists($reflector, 'isTrait') && $reflector->isTrait()) {
|
||||
return 'Trait Properties';
|
||||
} else {
|
||||
return 'Class Properties';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output style for the given property's visibility.
|
||||
*
|
||||
* @param \ReflectionProperty|ReflectionMagicProperty $property
|
||||
*/
|
||||
private function getVisibilityStyle(\Reflector $property): string
|
||||
{
|
||||
if ($property instanceof ReflectionMagicProperty) {
|
||||
return self::IS_VIRTUAL;
|
||||
}
|
||||
|
||||
if ($property->isPublic()) {
|
||||
return self::IS_PUBLIC;
|
||||
} elseif ($property->isProtected()) {
|
||||
return self::IS_PROTECTED;
|
||||
} else {
|
||||
return self::IS_PRIVATE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Present the $target's current value for a reflection property.
|
||||
*
|
||||
* @param \ReflectionProperty|ReflectionMagicProperty $property
|
||||
* @param mixed $target
|
||||
*/
|
||||
protected function presentValue(\Reflector $property, $target): string
|
||||
{
|
||||
// Magic properties use SignatureFormatter for display
|
||||
if ($property instanceof ReflectionMagicProperty) {
|
||||
return $this->presentSignature($property);
|
||||
}
|
||||
|
||||
if (!$target) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// If $target is a class or trait (try to) get the default
|
||||
// value for the property.
|
||||
if (!\is_object($target)) {
|
||||
try {
|
||||
$refl = new \ReflectionClass($target);
|
||||
$props = $refl->getDefaultProperties();
|
||||
if (\array_key_exists($property->name, $props)) {
|
||||
$suffix = $property->isStatic() ? '' : ' <aside>(default)</aside>';
|
||||
|
||||
return $this->presentRef($props[$property->name]).$suffix;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Well, we gave it a shot.
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
if (\PHP_VERSION_ID < 80100) {
|
||||
$property->setAccessible(true);
|
||||
}
|
||||
$value = $property->getValue($target);
|
||||
|
||||
return $this->presentRef($value);
|
||||
}
|
||||
}
|
||||
137
vendor/psy/psysh/src/Command/ListCommand/VariableEnumerator.php
vendored
Normal file
137
vendor/psy/psysh/src/Command/ListCommand/VariableEnumerator.php
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command\ListCommand;
|
||||
|
||||
use Psy\Context;
|
||||
use Psy\VarDumper\Presenter;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Variable Enumerator class.
|
||||
*/
|
||||
class VariableEnumerator extends Enumerator
|
||||
{
|
||||
// n.b. this array is the order in which special variables will be listed
|
||||
private const SPECIAL_NAMES = [
|
||||
'_', '_e', '__out', '__function', '__method', '__class', '__namespace', '__file', '__line', '__dir',
|
||||
];
|
||||
|
||||
private $context;
|
||||
|
||||
/**
|
||||
* Variable Enumerator constructor.
|
||||
*
|
||||
* Unlike most other enumerators, the Variable Enumerator needs access to
|
||||
* the current scope variables, so we need to pass it a Context instance.
|
||||
*
|
||||
* @param Presenter $presenter
|
||||
* @param Context $context
|
||||
*/
|
||||
public function __construct(Presenter $presenter, Context $context)
|
||||
{
|
||||
$this->context = $context;
|
||||
parent::__construct($presenter);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function listItems(InputInterface $input, ?\Reflector $reflector = null, $target = null): array
|
||||
{
|
||||
// only list variables when no Reflector is present.
|
||||
if ($reflector !== null || $target !== null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// only list variables if we are specifically asked
|
||||
if (!$input->getOption('vars')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$showAll = $input->getOption('all');
|
||||
$variables = $this->prepareVariables($this->getVariables($showAll));
|
||||
|
||||
if (empty($variables)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'Variables' => $variables,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scope variables.
|
||||
*
|
||||
* @param bool $showAll Include special variables (e.g. $_)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getVariables(bool $showAll): array
|
||||
{
|
||||
$scopeVars = $this->context->getAll();
|
||||
\uksort($scopeVars, function ($a, $b) {
|
||||
$aIndex = \array_search($a, self::SPECIAL_NAMES);
|
||||
$bIndex = \array_search($b, self::SPECIAL_NAMES);
|
||||
|
||||
if ($aIndex !== false) {
|
||||
if ($bIndex !== false) {
|
||||
return $aIndex - $bIndex;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($bIndex !== false) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return \strnatcasecmp($a, $b);
|
||||
});
|
||||
|
||||
$ret = [];
|
||||
foreach ($scopeVars as $name => $val) {
|
||||
if (!$showAll && \in_array($name, self::SPECIAL_NAMES)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ret[$name] = $val;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare formatted variable array.
|
||||
*
|
||||
* @param array $variables
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareVariables(array $variables): array
|
||||
{
|
||||
// My kingdom for a generator.
|
||||
$ret = [];
|
||||
foreach ($variables as $name => $val) {
|
||||
if ($this->showItem($name)) {
|
||||
$fname = '$'.$name;
|
||||
$ret[$fname] = [
|
||||
'name' => $fname,
|
||||
'style' => \in_array($name, self::SPECIAL_NAMES) ? self::IS_PRIVATE : self::IS_PUBLIC,
|
||||
'value' => $this->presentRef($val),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
142
vendor/psy/psysh/src/Command/ParseCommand.php
vendored
Normal file
142
vendor/psy/psysh/src/Command/ParseCommand.php
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\Error as PhpParserError;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Parser;
|
||||
use Psy\Context;
|
||||
use Psy\ContextAware;
|
||||
use Psy\Input\CodeArgument;
|
||||
use Psy\ParserFactory;
|
||||
use Psy\VarDumper\Presenter;
|
||||
use Psy\VarDumper\PresenterAware;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\VarDumper\Caster\Caster;
|
||||
|
||||
/**
|
||||
* Parse PHP code and show the abstract syntax tree.
|
||||
*/
|
||||
class ParseCommand extends Command implements ContextAware, PresenterAware
|
||||
{
|
||||
protected Context $context;
|
||||
private Presenter $presenter;
|
||||
private Parser $parser;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($name = null)
|
||||
{
|
||||
$this->parser = (new ParserFactory())->createParser();
|
||||
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* ContextAware interface.
|
||||
*
|
||||
* @param Context $context
|
||||
*/
|
||||
public function setContext(Context $context)
|
||||
{
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* PresenterAware interface.
|
||||
*
|
||||
* @param Presenter $presenter
|
||||
*/
|
||||
public function setPresenter(Presenter $presenter)
|
||||
{
|
||||
$this->presenter = clone $presenter;
|
||||
$this->presenter->addCasters([
|
||||
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;
|
||||
}
|
||||
|
||||
return $a;
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('parse')
|
||||
->setDefinition([
|
||||
new CodeArgument('code', CodeArgument::REQUIRED, 'PHP code to parse.'),
|
||||
new InputOption('depth', '', InputOption::VALUE_REQUIRED, 'Depth to parse.', 10),
|
||||
])
|
||||
->setDescription('Parse PHP code and show the abstract syntax tree.')
|
||||
->setHelp(
|
||||
<<<'HELP'
|
||||
Parse PHP code and show the abstract syntax tree.
|
||||
|
||||
This command is used in the development of PsySH. Given a string of PHP code,
|
||||
it pretty-prints the PHP Parser parse tree.
|
||||
|
||||
See https://github.com/nikic/PHP-Parser
|
||||
|
||||
It prolly won't be super useful for most of you, but it's here if you want to play.
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$code = $input->getArgument('code');
|
||||
$depth = $input->getOption('depth');
|
||||
|
||||
if (!\preg_match('/^\s*<\\?/', $code)) {
|
||||
$code = '<?php '.$code;
|
||||
}
|
||||
|
||||
try {
|
||||
$nodes = $this->parser->parse($code);
|
||||
} catch (PhpParserError $e) {
|
||||
if ($this->parseErrorIsEOF($e)) {
|
||||
$nodes = $this->parser->parse($code.';');
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
$this->shellOutput($output)->page($this->presenter->present($nodes, $depth, Presenter::RAW), OutputInterface::OUTPUT_RAW);
|
||||
|
||||
$this->context->setReturnValue($nodes);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function parseErrorIsEOF(PhpParserError $e): bool
|
||||
{
|
||||
$msg = $e->getRawMessage();
|
||||
|
||||
return ($msg === 'Unexpected token EOF') || (\strpos($msg, 'Syntax error, unexpected EOF') !== false);
|
||||
}
|
||||
}
|
||||
43
vendor/psy/psysh/src/Command/PsyVersionCommand.php
vendored
Normal file
43
vendor/psy/psysh/src/Command/PsyVersionCommand.php
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* A dumb little command for printing out the current Psy Shell version.
|
||||
*/
|
||||
class PsyVersionCommand extends Command
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('version')
|
||||
->setDefinition([])
|
||||
->setDescription('Show Psy Shell version.')
|
||||
->setHelp('Show Psy Shell version.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$output->writeln($this->getApplication()->getVersion());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
364
vendor/psy/psysh/src/Command/ReflectingCommand.php
vendored
Normal file
364
vendor/psy/psysh/src/Command/ReflectingCommand.php
vendored
Normal file
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\CodeCleaner;
|
||||
use Psy\CodeCleaner\NoReturnValue;
|
||||
use Psy\CodeCleanerAware;
|
||||
use Psy\Context;
|
||||
use Psy\ContextAware;
|
||||
use Psy\Exception\ErrorException;
|
||||
use Psy\Exception\RuntimeException;
|
||||
use Psy\Exception\UnexpectedTargetException;
|
||||
use Psy\Reflection\ReflectionConstant;
|
||||
use Psy\Sudo\SudoVisitor;
|
||||
use Psy\Util\Mirror;
|
||||
use Psy\Util\Str;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* An abstract command with helpers for inspecting the current context.
|
||||
*/
|
||||
abstract class ReflectingCommand extends Command implements ContextAware, CodeCleanerAware
|
||||
{
|
||||
const CLASS_OR_FUNC = '/^[\\\\\w]+$/';
|
||||
const CLASS_MEMBER = '/^([\\\\\w]+)::(\w+)$/';
|
||||
const CLASS_STATIC = '/^([\\\\\w]+)::\$(\w+)$/';
|
||||
const INSTANCE_MEMBER = '/^(\$\w+)(::|->)(\w+)$/';
|
||||
|
||||
protected Context $context;
|
||||
protected CodeCleaner $cleaner;
|
||||
private CodeArgumentParser $parser;
|
||||
private NodeTraverser $traverser;
|
||||
private Printer $printer;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($name = null)
|
||||
{
|
||||
$this->parser = new CodeArgumentParser();
|
||||
|
||||
// @todo Pass visitor directly to once we drop support for PHP-Parser 4.x
|
||||
$this->traverser = new NodeTraverser();
|
||||
$this->traverser->addVisitor(new SudoVisitor());
|
||||
|
||||
$this->printer = new Printer();
|
||||
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* ContextAware interface.
|
||||
*
|
||||
* @param Context $context
|
||||
*/
|
||||
public function setContext(Context $context)
|
||||
{
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodeCleanerAware interface.
|
||||
*/
|
||||
public function setCodeCleaner(CodeCleaner $cleaner)
|
||||
{
|
||||
$this->cleaner = $cleaner;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
protected function resolveName(string $name, bool $includeFunctions = false): string
|
||||
{
|
||||
$shell = $this->getShell();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Use CodeCleaner to resolve the name through use statements and namespace
|
||||
if (Str::isValidClassName($name)) {
|
||||
$resolved = $this->cleaner->resolveClassName($name);
|
||||
|
||||
// If we got a different name back, use it
|
||||
if ($resolved !== $name) {
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
// Fall back to the old resolveCode approach for edge cases
|
||||
try {
|
||||
$resolved = $this->resolveCode($name.'::class');
|
||||
if ($resolved !== $name) {
|
||||
return $resolved;
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
// Fall through to namespace check
|
||||
}
|
||||
}
|
||||
|
||||
if ($namespace = $shell->getNamespace()) {
|
||||
$fullName = $namespace.'\\'.$name;
|
||||
|
||||
if (\class_exists($fullName) || \interface_exists($fullName) || ($includeFunctions && \function_exists($fullName))) {
|
||||
return $fullName;
|
||||
}
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Reflector and documentation for a function, class or instance, constant, method or property.
|
||||
*
|
||||
* @param string $valueName Function, class, variable, constant, method or property name
|
||||
* @param OutputInterface|null $output Optional output for displaying cleaner messages
|
||||
*
|
||||
* @return array (value, Reflector)
|
||||
*/
|
||||
protected function getTargetAndReflector(string $valueName, ?OutputInterface $output = null): array
|
||||
{
|
||||
list($value, $member, $kind) = $this->getTarget($valueName);
|
||||
|
||||
// Display any implicit use statements that were added during name resolution
|
||||
if ($output !== null) {
|
||||
$this->writeCleanerMessages($output);
|
||||
}
|
||||
|
||||
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 {
|
||||
// Add an implicit `sudo` to target resolution.
|
||||
$nodes = $this->traverser->traverse($this->parser->parse($code));
|
||||
$sudoCode = $this->printer->prettyPrint($nodes);
|
||||
$value = $this->getShell()->execute($sudoCode, 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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:
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write log messages (e.g. implicit use statements) from CodeCleaner passes.
|
||||
*/
|
||||
protected function writeCleanerMessages(OutputInterface $output)
|
||||
{
|
||||
// Write to stderr if this is a ConsoleOutput
|
||||
if ($output instanceof ConsoleOutput) {
|
||||
$output = $output->getErrorOutput();
|
||||
}
|
||||
|
||||
foreach ($this->cleaner->getMessages() as $message) {
|
||||
$output->writeln(\sprintf('<whisper>%s</whisper>', OutputFormatter::escape($message)));
|
||||
}
|
||||
}
|
||||
}
|
||||
298
vendor/psy/psysh/src/Command/ShowCommand.php
vendored
Normal file
298
vendor/psy/psysh/src/Command/ShowCommand.php
vendored
Normal file
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use Psy\Exception\RuntimeException;
|
||||
use Psy\Exception\UnexpectedTargetException;
|
||||
use Psy\Formatter\CodeFormatter;
|
||||
use Psy\Formatter\SignatureFormatter;
|
||||
use Psy\Input\CodeArgument;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Show the code for an object, class, constant, method or property.
|
||||
*/
|
||||
class ShowCommand extends ReflectingCommand
|
||||
{
|
||||
private ?\Throwable $lastException = null;
|
||||
private ?int $lastExceptionIndex = null;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('show')
|
||||
->setDefinition([
|
||||
new CodeArgument('target', CodeArgument::OPTIONAL, 'Function, class, instance, constant, method or property to show.'),
|
||||
new InputOption('ex', null, InputOption::VALUE_OPTIONAL, 'Show last exception context. Optionally specify a stack index.', 1),
|
||||
])
|
||||
->setDescription('Show the code for an object, class, constant, method or property.')
|
||||
->setHelp(
|
||||
<<<HELP
|
||||
Show the code for an object, class, constant, method or property, or the context
|
||||
of the last exception.
|
||||
|
||||
<return>show --ex</return> defaults to showing the lines surrounding the location of the last
|
||||
exception. Invoking it more than once travels up the exception's stack trace,
|
||||
and providing a number shows the context of the given index of the trace.
|
||||
|
||||
e.g.
|
||||
<return>>>> show \$myObject</return>
|
||||
<return>>>> show Psy\Shell::debug</return>
|
||||
<return>>>> show --ex</return>
|
||||
<return>>>> show --ex 3</return>
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int 0 if everything went fine, or an exit code
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
// n.b. As far as I can tell, InputInterface doesn't want to tell me
|
||||
// whether an option with an optional value was actually passed. If you
|
||||
// call `$input->getOption('ex')`, it will return the default, both when
|
||||
// `--ex` is specified with no value, and when `--ex` isn't specified at
|
||||
// all.
|
||||
//
|
||||
// So we're doing something sneaky here. If we call `getOptions`, it'll
|
||||
// return the default value when `--ex` is not present, and `null` if
|
||||
// `--ex` is passed with no value. /shrug
|
||||
$opts = $input->getOptions();
|
||||
|
||||
// Strict comparison to `1` (the default value) here, because `--ex 1`
|
||||
// will come in as `"1"`. Now we can tell the difference between
|
||||
// "no --ex present", because it's the integer 1, "--ex with no value",
|
||||
// because it's `null`, and "--ex 1", because it's the string "1".
|
||||
if ($opts['ex'] !== 1) {
|
||||
if ($input->getArgument('target')) {
|
||||
throw new \InvalidArgumentException('Too many arguments (supply either "target" or "--ex")');
|
||||
}
|
||||
|
||||
$this->writeExceptionContext($input, $output);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($input->getArgument('target')) {
|
||||
$this->writeCodeContext($input, $output);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
throw new RuntimeException('Not enough arguments (missing: "target")');
|
||||
}
|
||||
|
||||
private function writeCodeContext(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$shellOutput = $this->shellOutput($output);
|
||||
|
||||
try {
|
||||
list($target, $reflector) = $this->getTargetAndReflector($input->getArgument('target'), $output);
|
||||
} 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),
|
||||
]);
|
||||
}
|
||||
|
||||
$shellOutput->page(CodeFormatter::formatCode($code));
|
||||
|
||||
return;
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// Set some magic local variables
|
||||
$this->setCommandScopeVariables($reflector);
|
||||
|
||||
try {
|
||||
$shellOutput->page(CodeFormatter::format($reflector));
|
||||
} catch (RuntimeException $e) {
|
||||
$output->writeln(SignatureFormatter::format($reflector));
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function writeExceptionContext(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$exception = $this->context->getLastException();
|
||||
if ($exception !== $this->lastException) {
|
||||
$this->lastException = null;
|
||||
$this->lastExceptionIndex = null;
|
||||
}
|
||||
|
||||
$opts = $input->getOptions();
|
||||
if ($opts['ex'] === null) {
|
||||
if ($this->lastException && $this->lastExceptionIndex !== null) {
|
||||
$index = $this->lastExceptionIndex + 1;
|
||||
} else {
|
||||
$index = 0;
|
||||
}
|
||||
} else {
|
||||
$index = \max(0, (int) $input->getOption('ex') - 1);
|
||||
}
|
||||
|
||||
$trace = $exception->getTrace();
|
||||
\array_unshift($trace, [
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
]);
|
||||
|
||||
if ($index >= \count($trace)) {
|
||||
$index = 0;
|
||||
}
|
||||
|
||||
$this->lastException = $exception;
|
||||
$this->lastExceptionIndex = $index;
|
||||
|
||||
$shell = $this->getShell();
|
||||
|
||||
$shell->writeExceptionHeader($output, $exception);
|
||||
$shell->writeSeparator($output);
|
||||
$this->writeTraceLine($output, $trace, $index);
|
||||
$shell->writeSpacer($output);
|
||||
$this->writeTraceCodeSnippet($output, $trace, $index);
|
||||
|
||||
$this->setCommandScopeVariablesFromContext($trace[$index]);
|
||||
}
|
||||
|
||||
private function writeTraceLine(OutputInterface $output, array $trace, $index)
|
||||
{
|
||||
$file = isset($trace[$index]['file']) ? $this->replaceCwd($trace[$index]['file']) : 'n/a';
|
||||
$line = isset($trace[$index]['line']) ? $trace[$index]['line'] : 'n/a';
|
||||
|
||||
$output->writeln(\sprintf(
|
||||
'From <info>%s:%d</info> at <strong>level %d</strong> of backtrace (of %d):',
|
||||
OutputFormatter::escape($file),
|
||||
OutputFormatter::escape($line),
|
||||
$index + 1,
|
||||
\count($trace)
|
||||
));
|
||||
}
|
||||
|
||||
private function replaceCwd(string $file): string
|
||||
{
|
||||
if ($cwd = \getcwd()) {
|
||||
$cwd = \rtrim($cwd, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
if ($cwd === false) {
|
||||
return $file;
|
||||
} else {
|
||||
return \preg_replace('/^'.\preg_quote($cwd, '/').'/', '', $file);
|
||||
}
|
||||
}
|
||||
|
||||
private function writeTraceCodeSnippet(OutputInterface $output, array $trace, $index)
|
||||
{
|
||||
if (!isset($trace[$index]['file'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$file = $trace[$index]['file'];
|
||||
if ($fileAndLine = $this->extractEvalFileAndLine($file)) {
|
||||
list($file, $line) = $fileAndLine;
|
||||
} else {
|
||||
if (!isset($trace[$index]['line'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$line = $trace[$index]['line'];
|
||||
}
|
||||
|
||||
if (\is_file($file)) {
|
||||
$code = @\file_get_contents($file);
|
||||
}
|
||||
|
||||
if (empty($code)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$startLine = \max($line - 5, 0);
|
||||
$endLine = $line + 5;
|
||||
|
||||
$output->write(CodeFormatter::formatCode($code, $startLine, $endLine, $line), false);
|
||||
}
|
||||
|
||||
private function setCommandScopeVariablesFromContext(array $context)
|
||||
{
|
||||
$vars = [];
|
||||
|
||||
if (isset($context['class'])) {
|
||||
$vars['__class'] = $context['class'];
|
||||
if (isset($context['function'])) {
|
||||
$vars['__method'] = $context['function'];
|
||||
}
|
||||
|
||||
try {
|
||||
$refl = new \ReflectionClass($context['class']);
|
||||
if ($namespace = $refl->getNamespaceName()) {
|
||||
$vars['__namespace'] = $namespace;
|
||||
}
|
||||
} catch (\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'])) {
|
||||
$file = $context['file'];
|
||||
if ($fileAndLine = $this->extractEvalFileAndLine($file)) {
|
||||
list($file, $line) = $fileAndLine;
|
||||
} elseif (isset($context['line'])) {
|
||||
$line = $context['line'];
|
||||
}
|
||||
|
||||
if (\is_file($file)) {
|
||||
$vars['__file'] = $file;
|
||||
if (isset($line)) {
|
||||
$vars['__line'] = $line;
|
||||
}
|
||||
$vars['__dir'] = \dirname($file);
|
||||
}
|
||||
}
|
||||
|
||||
$this->context->setCommandScopeVariables($vars);
|
||||
}
|
||||
|
||||
private function extractEvalFileAndLine(string $file)
|
||||
{
|
||||
if (\preg_match('/(.*)\\((\\d+)\\) : eval\\(\\)\'d code$/', $file, $matches)) {
|
||||
return [$matches[1], $matches[2]];
|
||||
}
|
||||
}
|
||||
}
|
||||
123
vendor/psy/psysh/src/Command/SudoCommand.php
vendored
Normal file
123
vendor/psy/psysh/src/Command/SudoCommand.php
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use PhpParser\NodeTraverser;
|
||||
use PhpParser\PrettyPrinter\Standard as Printer;
|
||||
use Psy\Input\CodeArgument;
|
||||
use Psy\Readline\Readline;
|
||||
use Psy\Readline\ReadlineAware;
|
||||
use Psy\Sudo\SudoVisitor;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Evaluate PHP code, bypassing visibility restrictions.
|
||||
*/
|
||||
class SudoCommand extends Command implements ReadlineAware
|
||||
{
|
||||
private Readline $readline;
|
||||
private CodeArgumentParser $parser;
|
||||
private NodeTraverser $traverser;
|
||||
private Printer $printer;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($name = null)
|
||||
{
|
||||
$this->parser = new CodeArgumentParser();
|
||||
|
||||
// @todo Pass visitor directly to once we drop support for PHP-Parser 4.x
|
||||
$this->traverser = new NodeTraverser();
|
||||
$this->traverser->addVisitor(new SudoVisitor());
|
||||
|
||||
$this->printer = new Printer();
|
||||
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Shell's Readline service.
|
||||
*
|
||||
* @param Readline $readline
|
||||
*/
|
||||
public function setReadline(Readline $readline)
|
||||
{
|
||||
$this->readline = $readline;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('sudo')
|
||||
->setDefinition([
|
||||
new CodeArgument('code', CodeArgument::REQUIRED, 'Code to execute.'),
|
||||
])
|
||||
->setDescription('Evaluate PHP code, bypassing visibility restrictions.')
|
||||
->setHelp(
|
||||
<<<'HELP'
|
||||
Evaluate PHP code, bypassing visibility restrictions.
|
||||
|
||||
e.g.
|
||||
<return>>>> $sekret->whisper("hi")</return>
|
||||
<return>PHP error: Call to private method Sekret::whisper() from context '' on line 1</return>
|
||||
|
||||
<return>>>> sudo $sekret->whisper("hi")</return>
|
||||
<return>=> "hi"</return>
|
||||
|
||||
<return>>>> $sekret->word</return>
|
||||
<return>PHP error: Cannot access private property Sekret::$word on line 1</return>
|
||||
|
||||
<return>>>> sudo $sekret->word</return>
|
||||
<return>=> "hi"</return>
|
||||
|
||||
<return>>>> $sekret->word = "please"</return>
|
||||
<return>PHP error: Cannot access private property Sekret::$word on line 1</return>
|
||||
|
||||
<return>>>> sudo $sekret->word = "please"</return>
|
||||
<return>=> "please"</return>
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int 0 if everything went fine, or an exit code
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$code = $input->getArgument('code');
|
||||
|
||||
// special case for !!
|
||||
if ($code === '!!') {
|
||||
$history = $this->readline->listHistory();
|
||||
if (\count($history) < 2) {
|
||||
throw new \InvalidArgumentException('No previous command to replay');
|
||||
}
|
||||
$code = $history[\count($history) - 2];
|
||||
}
|
||||
|
||||
$nodes = $this->traverser->traverse($this->parser->parse($code));
|
||||
|
||||
$sudoCode = $this->printer->prettyPrint($nodes);
|
||||
|
||||
$shell = $this->getShell();
|
||||
$shell->addCode($sudoCode, !$shell->hasCode());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
124
vendor/psy/psysh/src/Command/ThrowUpCommand.php
vendored
Normal file
124
vendor/psy/psysh/src/Command/ThrowUpCommand.php
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\PrettyPrinter\Standard as Printer;
|
||||
use Psy\Exception\ThrowUpException;
|
||||
use Psy\Input\CodeArgument;
|
||||
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
|
||||
{
|
||||
private CodeArgumentParser $parser;
|
||||
private Printer $printer;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($name = null)
|
||||
{
|
||||
$this->parser = new CodeArgumentParser();
|
||||
$this->printer = new Printer();
|
||||
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$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): int
|
||||
{
|
||||
$args = $this->prepareArgs($input->getArgument('exception'));
|
||||
$exception = new New_(new FullyQualifiedName(ThrowUpException::class), $args);
|
||||
$throwCode = 'throw '.$this->printer->prettyPrintExpr($exception).';';
|
||||
|
||||
$shell = $this->getShell();
|
||||
$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|null $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'))];
|
||||
}
|
||||
|
||||
$nodes = $this->parser->parse($code);
|
||||
if (\count($nodes) !== 1) {
|
||||
throw new \InvalidArgumentException('No idea how to throw this');
|
||||
}
|
||||
|
||||
$node = $nodes[0];
|
||||
$expr = $node->expr;
|
||||
|
||||
$args = [new Arg($expr, false, false, $node->getAttributes())];
|
||||
|
||||
// Allow throwing via a string, e.g. `throw-up "SUP"`
|
||||
if ($expr instanceof String_) {
|
||||
return [new Arg(new New_(new FullyQualifiedName(\Exception::class), $args))];
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
}
|
||||
176
vendor/psy/psysh/src/Command/TimeitCommand.php
vendored
Normal file
176
vendor/psy/psysh/src/Command/TimeitCommand.php
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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 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>';
|
||||
|
||||
// All times stored as nanoseconds (int on 64-bit, float on 32-bit overflow)
|
||||
/** @var int|float|null */
|
||||
private static $start = null;
|
||||
private static array $times = [];
|
||||
|
||||
private CodeArgumentParser $parser;
|
||||
private NodeTraverser $traverser;
|
||||
private Printer $printer;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($name = null)
|
||||
{
|
||||
$this->parser = new CodeArgumentParser();
|
||||
|
||||
// @todo Pass visitor directly to once we drop support for PHP-Parser 4.x
|
||||
$this->traverser = new NodeTraverser();
|
||||
$this->traverser->addVisitor(new TimeitVisitor());
|
||||
|
||||
$this->printer = new Printer();
|
||||
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$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): int
|
||||
{
|
||||
$code = $input->getArgument('code');
|
||||
$num = (int) ($input->getOption('num') ?: 1);
|
||||
|
||||
$shell = $this->getShell();
|
||||
|
||||
$instrumentedCode = $this->instrumentCode($code);
|
||||
|
||||
self::$times = [];
|
||||
|
||||
do {
|
||||
$_ = $shell->execute($instrumentedCode, true);
|
||||
$this->ensureEndMarked();
|
||||
} while (\count(self::$times) < $num);
|
||||
|
||||
$shell->writeReturnValue($_);
|
||||
|
||||
$times = self::$times;
|
||||
self::$times = [];
|
||||
|
||||
if ($num === 1) {
|
||||
// @phpstan-ignore-next-line offsetAccess.nonOffsetAccessible (guaranteed by loop: count($times) >= $num)
|
||||
$output->writeln(\sprintf(self::RESULT_MSG, $times[0] / 1e+9));
|
||||
} else {
|
||||
$total = \array_sum($times);
|
||||
\rsort($times);
|
||||
// @phpstan-ignore-next-line offsetAccess.nonOffsetAccessible (guaranteed by loop: count($times) >= $num)
|
||||
$median = $times[(int) \round($num / 2)];
|
||||
|
||||
$output->writeln(\sprintf(self::AVG_RESULT_MSG, ($total / $num) / 1e+9, $median / 1e+9, $total / 1e+9));
|
||||
}
|
||||
|
||||
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 = \hrtime(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[] = \hrtime(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.
|
||||
*/
|
||||
private function instrumentCode(string $code): string
|
||||
{
|
||||
return $this->printer->prettyPrint($this->traverser->traverse($this->parser->parse($code)));
|
||||
}
|
||||
}
|
||||
137
vendor/psy/psysh/src/Command/TimeitCommand/TimeitVisitor.php
vendored
Normal file
137
vendor/psy/psysh/src/Command/TimeitCommand/TimeitVisitor.php
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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 int $functionDepth = 0;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->functionDepth = 0;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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 null;
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int|Node|Node[]|null Replacement node (or special return value)
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if ($node instanceof FunctionLike) {
|
||||
$this->functionDepth--;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function afterTraverse(array $nodes)
|
||||
{
|
||||
// prepend a `markStart` call
|
||||
\array_unshift($nodes, new Expression($this->getStartCall(), []));
|
||||
|
||||
// append a `markEnd` call (wrapping the final node, if it's an expression)
|
||||
$last = $nodes[\count($nodes) - 1];
|
||||
if ($last instanceof Expr) {
|
||||
\array_pop($nodes);
|
||||
$nodes[] = $this->getEndCall($last);
|
||||
} elseif ($last instanceof Expression) {
|
||||
\array_pop($nodes);
|
||||
$nodes[] = new Expression($this->getEndCall($last->expr), $last->getAttributes());
|
||||
} elseif ($last instanceof Return_) {
|
||||
// nothing to do here, we're already ending with a return call
|
||||
} else {
|
||||
$nodes[] = new Expression($this->getEndCall(), []);
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
private function getEndCall(?Expr $arg = null): StaticCall
|
||||
{
|
||||
if ($arg === null) {
|
||||
$arg = NoReturnValue::create();
|
||||
}
|
||||
|
||||
return new StaticCall(new FullyQualifiedName(TimeitCommand::class), 'markEnd', [new Arg($arg)]);
|
||||
}
|
||||
}
|
||||
99
vendor/psy/psysh/src/Command/TraceCommand.php
vendored
Normal file
99
vendor/psy/psysh/src/Command/TraceCommand.php
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\ShellOutputAdapter;
|
||||
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(): void
|
||||
{
|
||||
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): int
|
||||
{
|
||||
$this->filter->bind($input);
|
||||
$trace = $this->getBacktrace(new \Exception(), $input->getOption('num'), $input->getOption('include-psy'));
|
||||
$this->shellOutput($output)->page($trace, ShellOutputAdapter::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|null $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);
|
||||
}
|
||||
}
|
||||
137
vendor/psy/psysh/src/Command/WhereamiCommand.php
vendored
Normal file
137
vendor/psy/psysh/src/Command/WhereamiCommand.php
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\ConfigPaths;
|
||||
use Psy\Formatter\CodeFormatter;
|
||||
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 array $backtrace;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->backtrace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$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): int
|
||||
{
|
||||
$shellOutput = $this->shellOutput($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;
|
||||
}
|
||||
|
||||
$shellOutput->startPaging();
|
||||
|
||||
$output->writeln(\sprintf('From <info>%s:%s</info>:', ConfigPaths::prettyPath($info['file']), $lineNum));
|
||||
$output->write(CodeFormatter::formatCode($code, $startLine, $endLine, $lineNum), false);
|
||||
|
||||
$shellOutput->stopPaging();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
130
vendor/psy/psysh/src/Command/WtfCommand.php
vendored
Normal file
130
vendor/psy/psysh/src/Command/WtfCommand.php
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use Psy\Context;
|
||||
use Psy\ContextAware;
|
||||
use Psy\Input\FilterOptions;
|
||||
use Psy\Output\ShellOutputAdapter;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Show the last uncaught exception.
|
||||
*/
|
||||
class WtfCommand extends TraceCommand implements ContextAware
|
||||
{
|
||||
protected Context $context;
|
||||
|
||||
/**
|
||||
* ContextAware interface.
|
||||
*
|
||||
* @param Context $context
|
||||
*/
|
||||
public function setContext(Context $context)
|
||||
{
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
list($grep, $insensitive, $invert) = FilterOptions::getOptions();
|
||||
|
||||
$this
|
||||
->setName('wtf')
|
||||
->setAliases(['last-exception', 'wtf?'])
|
||||
->setDefinition([
|
||||
new InputArgument('incredulity', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Number of lines to show.'),
|
||||
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show entire backtrace.'),
|
||||
|
||||
$grep,
|
||||
$insensitive,
|
||||
$invert,
|
||||
])
|
||||
->setDescription('Show the backtrace of the most recent exception.')
|
||||
->setHelp(
|
||||
<<<'HELP'
|
||||
Shows a few lines of the backtrace of the most recent exception.
|
||||
|
||||
If you want to see more lines, add more question marks or exclamation marks:
|
||||
|
||||
e.g.
|
||||
<return>>>> wtf ?</return>
|
||||
<return>>>> wtf ?!???!?!?</return>
|
||||
|
||||
To see the entire backtrace, pass the -a/--all flag:
|
||||
|
||||
e.g.
|
||||
<return>>>> wtf -a</return>
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int 0 if everything went fine, or an exit code
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->filter->bind($input);
|
||||
$shellOutput = $this->shellOutput($output);
|
||||
|
||||
$incredulity = \implode('', $input->getArgument('incredulity'));
|
||||
if (\strlen(\preg_replace('/[\\?!]/', '', $incredulity))) {
|
||||
throw new \InvalidArgumentException('Incredulity must include only "?" and "!"');
|
||||
}
|
||||
|
||||
$exception = $this->context->getLastException();
|
||||
$count = $input->getOption('all') ? \PHP_INT_MAX : \max(3, \pow(2, \strlen($incredulity) + 1));
|
||||
$shell = $this->getShell();
|
||||
|
||||
$shellOutput->startPaging();
|
||||
do {
|
||||
$traceCount = \count($exception->getTrace());
|
||||
$showLines = $count;
|
||||
// Show the whole trace if we'd only be hiding a few lines
|
||||
if ($traceCount < \max($count * 1.2, $count + 2)) {
|
||||
$showLines = \PHP_INT_MAX;
|
||||
}
|
||||
|
||||
$trace = $this->getBacktrace($exception, $showLines);
|
||||
$moreLines = $traceCount - \count($trace);
|
||||
|
||||
$shell->writeExceptionHeader($output, $exception);
|
||||
$shell->writeSeparator($output);
|
||||
$shellOutput->write($trace, true, ShellOutputAdapter::NUMBER_LINES);
|
||||
|
||||
if ($moreLines > 0) {
|
||||
$shell->writeSpacer($output);
|
||||
$output->writeln(\sprintf(
|
||||
'<aside>Use <return>wtf -a</return> to see %d more lines</aside>',
|
||||
$moreLines
|
||||
));
|
||||
}
|
||||
|
||||
$previous = $exception->getPrevious();
|
||||
if ($previous !== null) {
|
||||
$shell->writeSpacer($output);
|
||||
}
|
||||
} while ($exception = $previous);
|
||||
|
||||
$shellOutput->stopPaging();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
92
vendor/psy/psysh/src/Command/YoloCommand.php
vendored
Normal file
92
vendor/psy/psysh/src/Command/YoloCommand.php
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use Psy\Input\CodeArgument;
|
||||
use Psy\Readline\Readline;
|
||||
use Psy\Readline\ReadlineAware;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Execute code while bypassing reloader safety checks.
|
||||
*/
|
||||
class YoloCommand extends Command implements ReadlineAware
|
||||
{
|
||||
private Readline $readline;
|
||||
|
||||
/**
|
||||
* Set the Shell's Readline service.
|
||||
*/
|
||||
public function setReadline(Readline $readline)
|
||||
{
|
||||
$this->readline = $readline;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('yolo')
|
||||
->setDefinition([
|
||||
new CodeArgument('code', CodeArgument::REQUIRED, 'Code to execute, or !! to repeat last.'),
|
||||
])
|
||||
->setDescription('Execute code while bypassing reloader safety checks.')
|
||||
->setHelp(
|
||||
<<<'HELP'
|
||||
Execute code with all reloader safety checks bypassed.
|
||||
|
||||
When the reloader shows warnings about skipped conditionals or other
|
||||
risky operations, use yolo to force reload and execute anyway:
|
||||
|
||||
e.g.
|
||||
<return>>>> my_helper()</return>
|
||||
<return>Warning: Skipped conditional: if (...) { function my_helper() ... }</return>
|
||||
|
||||
<return>>>> yolo !!</return>
|
||||
<return>=> "result"</return>
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$code = $input->getArgument('code');
|
||||
|
||||
// Handle !! for last command
|
||||
if ($code === '!!') {
|
||||
$history = $this->readline->listHistory();
|
||||
\array_pop($history); // Remove the current `yolo !!` invocation
|
||||
$code = \end($history) ?: '';
|
||||
if (empty($code)) {
|
||||
throw new \RuntimeException('No previous command to repeat');
|
||||
}
|
||||
}
|
||||
|
||||
$shell = $this->getShell();
|
||||
|
||||
$shell->setForceReload(true);
|
||||
|
||||
try {
|
||||
$shell->addCode($code);
|
||||
|
||||
return 0;
|
||||
} finally {
|
||||
$shell->setForceReload(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
vendor/psy/psysh/src/CommandArgumentCompletionAware.php
vendored
Normal file
32
vendor/psy/psysh/src/CommandArgumentCompletionAware.php
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\Completion\AnalysisResult;
|
||||
|
||||
/**
|
||||
* Interface for commands that provide positional argument completion.
|
||||
*/
|
||||
interface CommandArgumentCompletionAware
|
||||
{
|
||||
/**
|
||||
* Whether this command owns completion for the current argument context.
|
||||
*/
|
||||
public function supportsArgumentCompletion(AnalysisResult $analysis): bool;
|
||||
|
||||
/**
|
||||
* Return completion candidates for the current command-tail context.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getArgumentCompletions(AnalysisResult $analysis): array;
|
||||
}
|
||||
30
vendor/psy/psysh/src/CommandAware.php
vendored
Normal file
30
vendor/psy/psysh/src/CommandAware.php
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\Command\Command;
|
||||
|
||||
/**
|
||||
* CommandAware interface.
|
||||
*
|
||||
* This interface is used to keep completion sources and matchers up to date
|
||||
* when commands are added to the Shell.
|
||||
*/
|
||||
interface CommandAware
|
||||
{
|
||||
/**
|
||||
* Set the available commands.
|
||||
*
|
||||
* @param Command[] $commands
|
||||
*/
|
||||
public function setCommands(array $commands);
|
||||
}
|
||||
44
vendor/psy/psysh/src/CommandMapTrait.php
vendored
Normal file
44
vendor/psy/psysh/src/CommandMapTrait.php
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 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\Command\Command;
|
||||
|
||||
/**
|
||||
* Trait for building a command lookup map (name + aliases → Command).
|
||||
*
|
||||
* Used by completion sources and refiners that need to resolve a command
|
||||
* name or alias to its Command instance.
|
||||
*/
|
||||
trait CommandMapTrait
|
||||
{
|
||||
/** @var array<string, Command> */
|
||||
private array $commandMap = [];
|
||||
|
||||
/**
|
||||
* Set the available commands.
|
||||
*
|
||||
* @param Command[] $commands
|
||||
*/
|
||||
public function setCommands(array $commands): void
|
||||
{
|
||||
$this->commandMap = [];
|
||||
|
||||
foreach ($commands as $command) {
|
||||
$this->commandMap[$command->getName()] = $command;
|
||||
|
||||
foreach ($command->getAliases() as $alias) {
|
||||
$this->commandMap[$alias] = $command;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
vendor/psy/psysh/src/Completion/AnalysisResult.php
vendored
Normal file
85
vendor/psy/psysh/src/Completion/AnalysisResult.php
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion;
|
||||
|
||||
use PhpParser\Node;
|
||||
|
||||
/**
|
||||
* Completion analysis result.
|
||||
*
|
||||
* Shared state for the completion pipeline.
|
||||
*
|
||||
* The analyzer establishes the initial context, refiners may narrow it, and
|
||||
* later stages reuse the same request metadata and parse-derived hints.
|
||||
*/
|
||||
class AnalysisResult
|
||||
{
|
||||
public int $kinds;
|
||||
public string $prefix;
|
||||
public ?string $leftSide;
|
||||
public ?Node $leftSideNode;
|
||||
/** @var string[] Fully-qualified class names (supports union types) */
|
||||
public array $leftSideTypes;
|
||||
public $leftSideValue;
|
||||
public string $input;
|
||||
/** @var array Tokenized input */
|
||||
public array $tokens;
|
||||
/** @var array Raw readline callback metadata, if available */
|
||||
public array $readlineInfo;
|
||||
/** @var bool Whether php-parser successfully parsed the input */
|
||||
public bool $parseSucceeded;
|
||||
|
||||
/**
|
||||
* @param string|string[]|null $leftSideTypes
|
||||
*/
|
||||
public function __construct(
|
||||
int $kinds,
|
||||
string $prefix = '',
|
||||
?string $leftSide = null,
|
||||
$leftSideTypes = [],
|
||||
$leftSideValue = null,
|
||||
array $tokens = [],
|
||||
string $input = '',
|
||||
?Node $leftSideNode = null,
|
||||
array $readlineInfo = [],
|
||||
bool $parseSucceeded = false
|
||||
) {
|
||||
$this->kinds = $kinds;
|
||||
$this->prefix = $prefix;
|
||||
$this->leftSide = $leftSide;
|
||||
$this->leftSideNode = $leftSideNode;
|
||||
$this->leftSideTypes = (array) $leftSideTypes;
|
||||
$this->leftSideValue = $leftSideValue;
|
||||
$this->tokens = $tokens;
|
||||
$this->input = $input;
|
||||
$this->readlineInfo = $readlineInfo;
|
||||
$this->parseSucceeded = $parseSucceeded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a copy with updated completion context for a later pipeline stage.
|
||||
*/
|
||||
public function withContext(int $kinds, string $prefix = '', ?string $leftSide = null, ?Node $leftSideNode = null): self
|
||||
{
|
||||
// Types and value are cleared because CompletionEngine re-resolves
|
||||
// them after all refiners have run.
|
||||
$copy = clone $this;
|
||||
$copy->kinds = $kinds;
|
||||
$copy->prefix = $prefix;
|
||||
$copy->leftSide = $leftSide;
|
||||
$copy->leftSideNode = $leftSideNode;
|
||||
$copy->leftSideTypes = [];
|
||||
$copy->leftSideValue = null;
|
||||
|
||||
return $copy;
|
||||
}
|
||||
}
|
||||
218
vendor/psy/psysh/src/Completion/CompletionEngine.php
vendored
Normal file
218
vendor/psy/psysh/src/Completion/CompletionEngine.php
vendored
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion;
|
||||
|
||||
use PhpParser\Node as AstNode;
|
||||
use Psy\CodeCleaner;
|
||||
use Psy\Completion\Refiner\AnalysisRefinerInterface;
|
||||
use Psy\Completion\Refiner\CommandSyntaxRefiner;
|
||||
use Psy\Completion\Refiner\PartialInputRefiner;
|
||||
use Psy\Completion\Source\CatalogSource;
|
||||
use Psy\Completion\Source\ClassConstantSource;
|
||||
use Psy\Completion\Source\KeywordSource;
|
||||
use Psy\Completion\Source\MagicMethodSource;
|
||||
use Psy\Completion\Source\MagicPropertySource;
|
||||
use Psy\Completion\Source\MethodSource;
|
||||
use Psy\Completion\Source\NamespaceSource;
|
||||
use Psy\Completion\Source\ObjectMethodSource;
|
||||
use Psy\Completion\Source\ObjectPropertySource;
|
||||
use Psy\Completion\Source\PropertySource;
|
||||
use Psy\Completion\Source\SourceInterface;
|
||||
use Psy\Completion\Source\StaticMethodSource;
|
||||
use Psy\Completion\Source\StaticPropertySource;
|
||||
use Psy\Completion\Source\VariableSource;
|
||||
use Psy\Context;
|
||||
use Psy\Readline\Interactive\Helper\DebugLog;
|
||||
|
||||
/**
|
||||
* Completion pipeline coordinator.
|
||||
*
|
||||
* Each stage focuses on one job: parse the input, refine the context, then
|
||||
* collect candidates from applicable sources.
|
||||
*/
|
||||
class CompletionEngine
|
||||
{
|
||||
private Context $context;
|
||||
private ContextAnalyzer $analyzer;
|
||||
private TypeResolver $typeResolver;
|
||||
private SymbolCatalog $symbolCatalog;
|
||||
|
||||
/** @var SourceInterface[] */
|
||||
private array $sources = [];
|
||||
|
||||
/** @var AnalysisRefinerInterface[] */
|
||||
private array $refiners = [];
|
||||
|
||||
public function __construct(Context $context, ?CodeCleaner $cleaner = null, ?SymbolCatalog $symbolCatalog = null)
|
||||
{
|
||||
$this->context = $context;
|
||||
$this->analyzer = new ContextAnalyzer($cleaner);
|
||||
$this->typeResolver = new TypeResolver($context, $cleaner);
|
||||
$this->symbolCatalog = $symbolCatalog ?? new SymbolCatalog();
|
||||
$this->addRefiner(new PartialInputRefiner());
|
||||
$this->addRefiner(new CommandSyntaxRefiner());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the standard PsySH completion source set.
|
||||
*
|
||||
* @param SourceInterface[] $additionalSources Pre-initialized sources to include
|
||||
*/
|
||||
public function registerDefaultSources(array $additionalSources = []): void
|
||||
{
|
||||
// Context-aware sources.
|
||||
$this->addSource(new VariableSource($this->context));
|
||||
$this->addSource(new ObjectMethodSource());
|
||||
$this->addSource(new ObjectPropertySource());
|
||||
|
||||
// Static reflection sources.
|
||||
$this->addSource(new MethodSource());
|
||||
$this->addSource(new PropertySource());
|
||||
$this->addSource(new StaticMethodSource());
|
||||
$this->addSource(new StaticPropertySource());
|
||||
$this->addSource(new ClassConstantSource());
|
||||
|
||||
// Docblock magic sources.
|
||||
$this->addSource(new MagicMethodSource());
|
||||
$this->addSource(new MagicPropertySource());
|
||||
|
||||
// Symbol sources (shared symbol catalog snapshot cache).
|
||||
$this->addSource(new CatalogSource(CompletionKind::CLASS_NAME, [$this->symbolCatalog, 'getClasses'], $this->symbolCatalog));
|
||||
$this->addSource(new CatalogSource(CompletionKind::INTERFACE_NAME, [$this->symbolCatalog, 'getInterfaces'], $this->symbolCatalog));
|
||||
$this->addSource(new CatalogSource(CompletionKind::TRAIT_NAME, [$this->symbolCatalog, 'getTraits'], $this->symbolCatalog));
|
||||
$this->addSource(new CatalogSource(CompletionKind::ATTRIBUTE_NAME, [$this->symbolCatalog, 'getAttributeClasses'], $this->symbolCatalog));
|
||||
$this->addSource(new CatalogSource(CompletionKind::FUNCTION_NAME, [$this->symbolCatalog, 'getFunctions'], $this->symbolCatalog));
|
||||
$this->addSource(new CatalogSource(CompletionKind::CONSTANT, [$this->symbolCatalog, 'getConstants'], $this->symbolCatalog));
|
||||
$this->addSource(new NamespaceSource($this->symbolCatalog));
|
||||
|
||||
// Additional pre-initialized sources.
|
||||
foreach ($additionalSources as $source) {
|
||||
$this->addSource($source);
|
||||
}
|
||||
|
||||
// Generic sources.
|
||||
$this->addSource(new KeywordSource());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a completion source.
|
||||
*/
|
||||
public function addSource(SourceInterface $source): void
|
||||
{
|
||||
if (!\in_array($source, $this->sources, true)) {
|
||||
$this->sources[] = $source;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an analysis refiner.
|
||||
*
|
||||
* Refiners translate broad parser output into the narrower completion lanes
|
||||
* that sources consume.
|
||||
*/
|
||||
public function addRefiner(AnalysisRefinerInterface $refiner): void
|
||||
{
|
||||
if (!\in_array($refiner, $this->refiners, true)) {
|
||||
$this->refiners[] = $refiner;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get completions for a normalized request.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getCompletions(CompletionRequest $request): array
|
||||
{
|
||||
$start = \microtime(true);
|
||||
DebugLog::log('Completion', 'START', [
|
||||
'mode' => $request->getMode(),
|
||||
'input' => $request->getBuffer(),
|
||||
'cursor' => $request->getCursor(),
|
||||
]);
|
||||
|
||||
$analysis = $this->analyzer->analyze(
|
||||
$request->getBuffer(),
|
||||
$request->getCursor(),
|
||||
$request->getReadlineInfo()
|
||||
);
|
||||
foreach ($this->refiners as $refiner) {
|
||||
$analysis = $refiner->refine($analysis);
|
||||
}
|
||||
DebugLog::log('Completion', 'ANALYZED', [
|
||||
'kinds' => $analysis->kinds,
|
||||
'prefix' => $analysis->prefix,
|
||||
'leftSide' => $analysis->leftSide ?? 'null',
|
||||
]);
|
||||
|
||||
$leftSide = $analysis->leftSide;
|
||||
if ($leftSide !== null) {
|
||||
$leftSideNode = $analysis->leftSideNode;
|
||||
$analysis->leftSideTypes = $leftSideNode instanceof AstNode
|
||||
? $this->typeResolver->resolveNodeTypes($leftSideNode, $leftSide)
|
||||
: $this->typeResolver->resolveTypes($leftSide);
|
||||
$analysis->leftSideValue = $this->typeResolver->resolveValue($leftSide);
|
||||
DebugLog::log('Completion', 'RESOLVED_TYPES', [
|
||||
'types' => empty($analysis->leftSideTypes) ? 'none' : \implode('|', $analysis->leftSideTypes),
|
||||
'has_value' => $analysis->leftSideValue !== null,
|
||||
]);
|
||||
}
|
||||
|
||||
$results = $this->collectFromSources($analysis);
|
||||
$results = \array_values(\array_unique(\array_filter($results, fn ($match) => $match !== '' && $match !== null)));
|
||||
|
||||
if ($analysis->prefix !== '' && !empty($results)) {
|
||||
$before = \count($results);
|
||||
$results = FuzzyMatcher::filter($analysis->prefix, $results);
|
||||
DebugLog::log('Completion', 'FUZZY_FILTER', [
|
||||
'before' => $before,
|
||||
'after' => \count($results),
|
||||
]);
|
||||
}
|
||||
|
||||
$latencyMs = (\microtime(true) - $start) * 1000;
|
||||
DebugLog::log('Completion', 'METRICS', [
|
||||
'latency_ms' => \round($latencyMs, 2),
|
||||
'results' => \count($results),
|
||||
]);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect completions from all applicable sources.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function collectFromSources(AnalysisResult $analysis): array
|
||||
{
|
||||
$completions = [];
|
||||
|
||||
foreach ($this->sources as $source) {
|
||||
if (!$source->appliesToKind($analysis->kinds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sourceCompletions = $source->getCompletions($analysis);
|
||||
if (!empty($sourceCompletions)) {
|
||||
DebugLog::log('Completion', 'SOURCE_MATCHED', [
|
||||
'source' => \get_class($source),
|
||||
'count' => \count($sourceCompletions),
|
||||
]);
|
||||
|
||||
$completions = \array_merge($completions, $sourceCompletions);
|
||||
}
|
||||
}
|
||||
|
||||
return $completions;
|
||||
}
|
||||
}
|
||||
75
vendor/psy/psysh/src/Completion/CompletionKind.php
vendored
Normal file
75
vendor/psy/psysh/src/Completion/CompletionKind.php
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion;
|
||||
|
||||
/**
|
||||
* Completion kind bitmask constants.
|
||||
*
|
||||
* Defines the syntactic kinds where completion is being requested.
|
||||
* Multiple kinds can be combined using bitwise OR to represent
|
||||
* positions where multiple types of completions are valid.
|
||||
*
|
||||
* Example:
|
||||
* $kinds = CompletionKind::CLASS_NAME | CompletionKind::FUNCTION_NAME;
|
||||
*/
|
||||
class CompletionKind
|
||||
{
|
||||
// Special
|
||||
public const NONE = 0;
|
||||
public const UNKNOWN = 1 << 0;
|
||||
|
||||
// Variables
|
||||
public const VARIABLE = 1 << 1; // $foo|
|
||||
|
||||
// Object members (instance)
|
||||
public const OBJECT_METHOD = 1 << 2; // $foo->method()|
|
||||
public const OBJECT_PROPERTY = 1 << 3; // $foo->property|
|
||||
|
||||
// Static members (class)
|
||||
public const STATIC_METHOD = 1 << 4; // Foo::method()|
|
||||
public const STATIC_PROPERTY = 1 << 5; // Foo::$property|
|
||||
public const CLASS_CONSTANT = 1 << 6; // Foo::CONSTANT|
|
||||
|
||||
// Type names
|
||||
public const CLASS_NAME = 1 << 7; // new Foo|, extends Foo|
|
||||
public const INTERFACE_NAME = 1 << 8; // implements Bar|
|
||||
public const TRAIT_NAME = 1 << 9; // use SomeTrait| (inside class)
|
||||
public const ATTRIBUTE_NAME = 1 << 10; // #[Deprecated|] (PHP 8+)
|
||||
|
||||
// Global symbols
|
||||
public const FUNCTION_NAME = 1 << 11; // foo|()
|
||||
public const CONSTANT = 1 << 12; // CONST|
|
||||
public const NAMESPACE = 1 << 13; // namespace Foo\|, use Foo\|
|
||||
|
||||
// PsySH-specific
|
||||
public const COMMAND = 1 << 14; // ls|, doc|
|
||||
public const COMMAND_OPTION = 1 << 15; // ls --option|, ls -a|
|
||||
|
||||
// PHP keywords
|
||||
public const KEYWORD = 1 << 16; // echo|, isset|
|
||||
|
||||
// Advanced (future)
|
||||
public const NAMED_PARAMETER = 1 << 17; // foo(name: |) (PHP 8+)
|
||||
public const ARRAY_KEY = 1 << 18; // $array['key'|]
|
||||
public const COMMAND_ARGUMENT = 1 << 19; // config set verbosity|
|
||||
|
||||
// Common combinations for ambiguous contexts
|
||||
public const OBJECT_MEMBER = self::OBJECT_METHOD | self::OBJECT_PROPERTY; // $foo->|
|
||||
public const STATIC_MEMBER = self::STATIC_METHOD | self::STATIC_PROPERTY | self::CLASS_CONSTANT; // Foo::|
|
||||
public const TYPE_NAME = self::CLASS_NAME | self::INTERFACE_NAME; // Type hints, return types
|
||||
public const CLASS_LIKE = self::CLASS_NAME | self::INTERFACE_NAME | self::TRAIT_NAME; // Any class-like structure
|
||||
public const CALLABLE = self::FUNCTION_NAME | self::CLASS_NAME;
|
||||
public const SYMBOL = self::FUNCTION_NAME | self::CLASS_LIKE | self::CONSTANT;
|
||||
|
||||
// Contexts where the input might be a shell command rather than PHP code
|
||||
public const COMMAND_ELIGIBLE = self::UNKNOWN | self::SYMBOL | self::KEYWORD;
|
||||
}
|
||||
68
vendor/psy/psysh/src/Completion/CompletionRequest.php
vendored
Normal file
68
vendor/psy/psysh/src/Completion/CompletionRequest.php
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion;
|
||||
|
||||
/**
|
||||
* Normalized completion request.
|
||||
*
|
||||
* Uses the full buffer and an absolute cursor position so callers can request
|
||||
* completions from any position, including multiline input.
|
||||
*/
|
||||
class CompletionRequest
|
||||
{
|
||||
public const MODE_TAB = 'tab';
|
||||
public const MODE_SUGGESTION = 'suggestion';
|
||||
|
||||
private string $buffer;
|
||||
private int $cursor;
|
||||
private string $mode;
|
||||
/** @var array Raw callback metadata from the caller, if any */
|
||||
private array $readlineInfo;
|
||||
|
||||
public function __construct(string $buffer, int $cursor, string $mode = self::MODE_TAB, array $readlineInfo = [])
|
||||
{
|
||||
$this->buffer = $buffer;
|
||||
$this->cursor = $this->normalizeCursor($buffer, $cursor);
|
||||
$this->mode = $mode;
|
||||
$this->readlineInfo = $readlineInfo;
|
||||
}
|
||||
|
||||
public function getBuffer(): string
|
||||
{
|
||||
return $this->buffer;
|
||||
}
|
||||
|
||||
public function getCursor(): int
|
||||
{
|
||||
return $this->cursor;
|
||||
}
|
||||
|
||||
public function getMode(): string
|
||||
{
|
||||
return $this->mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw readline callback metadata associated with this request.
|
||||
*/
|
||||
public function getReadlineInfo(): array
|
||||
{
|
||||
return $this->readlineInfo;
|
||||
}
|
||||
|
||||
private function normalizeCursor(string $buffer, int $cursor): int
|
||||
{
|
||||
$length = \mb_strlen($buffer);
|
||||
|
||||
return \max(0, \min($cursor, $length));
|
||||
}
|
||||
}
|
||||
221
vendor/psy/psysh/src/Completion/ContextAnalyzer.php
vendored
Normal file
221
vendor/psy/psysh/src/Completion/ContextAnalyzer.php
vendored
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion;
|
||||
|
||||
use PhpParser\Error as PhpParserError;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\ClassConstFetch;
|
||||
use PhpParser\Node\Expr\ConstFetch;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Expr\MethodCall;
|
||||
use PhpParser\Node\Expr\New_;
|
||||
use PhpParser\Node\Expr\NullsafeMethodCall;
|
||||
use PhpParser\Node\Expr\NullsafePropertyFetch;
|
||||
use PhpParser\Node\Expr\PropertyFetch;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PhpParser\Node\Expr\StaticPropertyFetch;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use PhpParser\Node\Identifier;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\NodeTraverser;
|
||||
use PhpParser\Parser;
|
||||
use PhpParser\PrettyPrinter\Standard as Printer;
|
||||
use Psy\CodeCleaner;
|
||||
use Psy\ParserFactory;
|
||||
|
||||
/**
|
||||
* Analyzes input to determine completion context.
|
||||
*
|
||||
* This stage provides the parser-derived starting point for completion. Its
|
||||
* job is to describe the PHP syntax at the cursor, not to decide every higher-
|
||||
* level completion mode built on top of that syntax.
|
||||
*/
|
||||
class ContextAnalyzer
|
||||
{
|
||||
private Parser $parser;
|
||||
private Printer $printer;
|
||||
private ?CodeCleaner $cleaner;
|
||||
|
||||
public function __construct(?CodeCleaner $cleaner = null)
|
||||
{
|
||||
$this->parser = (new ParserFactory())->createParser();
|
||||
$this->printer = new Printer();
|
||||
$this->cleaner = $cleaner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze input and return the coarse parser-derived context.
|
||||
*/
|
||||
public function analyze(string $input, int $cursor, array $readlineInfo = []): AnalysisResult
|
||||
{
|
||||
// Cursor is in code-point units, so use mb_substr
|
||||
$inputToCursor = \mb_substr($input, 0, $cursor);
|
||||
$cursorAtEnd = ($cursor >= \mb_strlen($input));
|
||||
$analysis = $this->tryParse($inputToCursor, $cursorAtEnd);
|
||||
$parseSucceeded = $analysis !== null;
|
||||
$analysis = $analysis ?? new AnalysisResult(CompletionKind::UNKNOWN, '');
|
||||
$analysis->parseSucceeded = $parseSucceeded;
|
||||
|
||||
$analysis->input = $inputToCursor;
|
||||
$analysis->tokens = @\token_get_all('<?php '.$inputToCursor);
|
||||
$analysis->readlineInfo = $readlineInfo;
|
||||
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse input and analyze the resulting AST.
|
||||
*/
|
||||
private function tryParse(string $input, bool $cursorAtEnd): ?AnalysisResult
|
||||
{
|
||||
$code = '<?php '.$input;
|
||||
|
||||
// Try with semicolon first (most common case), then without
|
||||
try {
|
||||
$stmts = $this->parser->parse($code.';');
|
||||
} catch (PhpParserError $e) {
|
||||
try {
|
||||
$stmts = $this->parser->parse($code);
|
||||
} catch (PhpParserError $e2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($stmts)) {
|
||||
return new AnalysisResult(CompletionKind::UNKNOWN, '');
|
||||
}
|
||||
|
||||
$visitor = new DeepestNodeVisitor();
|
||||
$traverser = new NodeTraverser();
|
||||
$traverser->addVisitor($visitor);
|
||||
$traverser->traverse($stmts);
|
||||
|
||||
$node = $visitor->getDeepestNode();
|
||||
if ($node === null) {
|
||||
return new AnalysisResult(CompletionKind::UNKNOWN, '');
|
||||
}
|
||||
|
||||
// Trailing whitespace means the user has moved past this token
|
||||
if ($cursorAtEnd && $input !== \rtrim($input)) {
|
||||
return new AnalysisResult(CompletionKind::UNKNOWN, '');
|
||||
}
|
||||
|
||||
return $this->analyzeNode($node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a specific AST node to determine completion context.
|
||||
*/
|
||||
private function analyzeNode(Node $node): AnalysisResult
|
||||
{
|
||||
// $foo->bar, $foo->bar(), $foo?->bar, $foo?->bar()
|
||||
if (
|
||||
$node instanceof MethodCall
|
||||
|| $node instanceof PropertyFetch
|
||||
|| $node instanceof NullsafeMethodCall
|
||||
|| $node instanceof NullsafePropertyFetch
|
||||
) {
|
||||
$leftSide = $this->extractExpression($node->var);
|
||||
$prefix = $node->name instanceof Identifier ? $node->name->name : '';
|
||||
$result = new AnalysisResult(CompletionKind::OBJECT_MEMBER, $prefix, $leftSide);
|
||||
$result->leftSideNode = $node->var;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Foo::bar(), Foo::BAR, Foo::$bar
|
||||
if (
|
||||
$node instanceof StaticCall
|
||||
|| $node instanceof ClassConstFetch
|
||||
|| $node instanceof StaticPropertyFetch
|
||||
) {
|
||||
$leftSide = $this->extractExpression($node->class);
|
||||
$prefix = $node->name instanceof Identifier ? $node->name->name : '';
|
||||
$result = new AnalysisResult(CompletionKind::STATIC_MEMBER, $prefix, $leftSide);
|
||||
$result->leftSideNode = $node->class;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// new Foo
|
||||
if ($node instanceof New_) {
|
||||
$prefix = $node->class instanceof Name ? $node->class->toString() : '';
|
||||
|
||||
return new AnalysisResult(CompletionKind::CLASS_NAME, $prefix);
|
||||
}
|
||||
|
||||
// $foo
|
||||
if ($node instanceof Variable) {
|
||||
$prefix = \is_string($node->name) ? $node->name : '';
|
||||
|
||||
return new AnalysisResult(CompletionKind::VARIABLE, $prefix);
|
||||
}
|
||||
|
||||
// foo()
|
||||
if ($node instanceof FuncCall && $node->name instanceof Name) {
|
||||
return new AnalysisResult(CompletionKind::FUNCTION_NAME, $node->name->toString());
|
||||
}
|
||||
|
||||
// FOO (could be a constant, function, or class reference)
|
||||
if ($node instanceof ConstFetch && $node->name instanceof Name) {
|
||||
return new AnalysisResult(CompletionKind::SYMBOL, $node->name->toString());
|
||||
}
|
||||
|
||||
if ($node instanceof Identifier) {
|
||||
return new AnalysisResult(CompletionKind::UNKNOWN, $node->name);
|
||||
}
|
||||
|
||||
// Bare name: foo or Foo\Bar
|
||||
if ($node instanceof Name) {
|
||||
return new AnalysisResult(CompletionKind::SYMBOL, $node->toString());
|
||||
}
|
||||
|
||||
return new AnalysisResult(CompletionKind::UNKNOWN, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a string representation of an expression.
|
||||
*
|
||||
* Uses the printer to convert the AST node back to code.
|
||||
*/
|
||||
private function extractExpression($expr): string
|
||||
{
|
||||
if ($expr instanceof Variable && \is_string($expr->name)) {
|
||||
return '$'.$expr->name;
|
||||
}
|
||||
|
||||
if ($expr instanceof Name) {
|
||||
return $this->resolveClassName($expr);
|
||||
}
|
||||
|
||||
if ($expr instanceof Node\Expr) {
|
||||
return $this->printer->prettyPrintExpr($expr);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a class name using CodeCleaner's namespace context.
|
||||
*
|
||||
* This ensures we use the same use statements and namespace as the code
|
||||
* being executed.
|
||||
*/
|
||||
private function resolveClassName(Name $name): string
|
||||
{
|
||||
if ($this->cleaner === null) {
|
||||
return $name->toString();
|
||||
}
|
||||
|
||||
return $this->cleaner->resolveClassName($name->toString());
|
||||
}
|
||||
}
|
||||
48
vendor/psy/psysh/src/Completion/DeepestNodeVisitor.php
vendored
Normal file
48
vendor/psy/psysh/src/Completion/DeepestNodeVisitor.php
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt\Expression;
|
||||
use PhpParser\NodeTraverser;
|
||||
use PhpParser\NodeVisitorAbstract;
|
||||
|
||||
/**
|
||||
* Visitor to find the top-level expression node in the statement.
|
||||
*
|
||||
* We want the outermost expression (PropertyFetch, MethodCall, New_, etc.)
|
||||
* not their child nodes. For a statement like `$baz->format;`, we want the
|
||||
* PropertyFetch node, not the Variable node inside it.
|
||||
*/
|
||||
class DeepestNodeVisitor extends NodeVisitorAbstract
|
||||
{
|
||||
private ?Node $targetNode = null;
|
||||
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
// If this is an Expression statement, grab its expression
|
||||
if ($node instanceof Expression) {
|
||||
$this->targetNode = $node->expr;
|
||||
|
||||
// Don't traverse into the expression - we have what we need
|
||||
// @phan-suppress-next-line PhanDeprecatedClassConstant - keep compat with php-parser 4.x baseline
|
||||
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getDeepestNode(): ?Node
|
||||
{
|
||||
return $this->targetNode;
|
||||
}
|
||||
}
|
||||
176
vendor/psy/psysh/src/Completion/FuzzyMatcher.php
vendored
Normal file
176
vendor/psy/psysh/src/Completion/FuzzyMatcher.php
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion;
|
||||
|
||||
/**
|
||||
* Fuzzy matching utility for tab completion.
|
||||
*
|
||||
* Matches candidates where the search string's characters appear in order,
|
||||
* similar to Fish shell and modern IDE fuzzy completion.
|
||||
*
|
||||
* Examples:
|
||||
* - "asum" matches "array_sum"
|
||||
* - "stl" matches "strtolower"
|
||||
* - "AE" matches "ArrayException" (case-insensitive)
|
||||
*/
|
||||
class FuzzyMatcher
|
||||
{
|
||||
/**
|
||||
* Filter candidates using fuzzy matching.
|
||||
*
|
||||
* Returns candidates where all characters in the search string appear
|
||||
* in order within the candidate string (case-insensitive).
|
||||
*
|
||||
* Results are sorted by match quality (exact prefix matches first,
|
||||
* then by how early the match starts).
|
||||
*
|
||||
* @param string $search Search string (e.g., "asum")
|
||||
* @param string[] $candidates Array of candidates to filter
|
||||
*
|
||||
* @return string[] Filtered and sorted candidates
|
||||
*/
|
||||
public static function filter(string $search, array $candidates): array
|
||||
{
|
||||
if ($search === '') {
|
||||
$sorted = $candidates;
|
||||
\sort($sorted);
|
||||
|
||||
return $sorted;
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
foreach ($candidates as $candidate) {
|
||||
$score = self::match($search, $candidate);
|
||||
if ($score !== null) {
|
||||
$matches[] = ['candidate' => $candidate, 'score' => $score];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score (lower is better), then alphabetically
|
||||
\usort($matches, function ($a, $b) {
|
||||
if ($a['score'] !== $b['score']) {
|
||||
return $a['score'] <=> $b['score'];
|
||||
}
|
||||
|
||||
return \strcmp($a['candidate'], $b['candidate']);
|
||||
});
|
||||
|
||||
return \array_column($matches, 'candidate');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if search matches candidate and return a quality score.
|
||||
*
|
||||
* Returns null if no match, or a numeric score where lower is better.
|
||||
* Score factors:
|
||||
* - Exact prefix match gets lowest score (best)
|
||||
* - Earlier matches get better scores
|
||||
* - Consecutive character matches get bonus
|
||||
* - First character must match at start or after a word boundary
|
||||
*
|
||||
* @return int|null Match score (lower is better), or null if no match
|
||||
*/
|
||||
private static function match(string $search, string $candidate): ?int
|
||||
{
|
||||
$searchLen = \strlen($search);
|
||||
$candidateLen = \strlen($candidate);
|
||||
|
||||
if ($searchLen === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$searchLower = \strtolower($search);
|
||||
$candidateLower = \strtolower($candidate);
|
||||
|
||||
// Check for exact prefix match first (best score)
|
||||
if (\strpos($candidateLower, $searchLower) === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if it contains the search as a substring (very good score)
|
||||
$substringPos = \strpos($candidateLower, $searchLower);
|
||||
if ($substringPos !== false) {
|
||||
// Only match if substring starts at a word boundary
|
||||
if ($substringPos === 0 || self::isWordBoundary($candidate[$substringPos - 1])) {
|
||||
return $substringPos + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Fuzzy match: characters must appear in order
|
||||
// First character MUST match at start or after a word boundary
|
||||
$searchIdx = 0;
|
||||
$candidateIdx = 0;
|
||||
$lastMatchIdx = -1;
|
||||
$firstMatchIdx = null;
|
||||
$consecutiveMatches = 0;
|
||||
$firstCharFound = false;
|
||||
|
||||
while ($searchIdx < $searchLen && $candidateIdx < $candidateLen) {
|
||||
if ($searchLower[$searchIdx] === $candidateLower[$candidateIdx]) {
|
||||
// For the first character, ensure it's at a word boundary
|
||||
if ($searchIdx === 0) {
|
||||
if ($candidateIdx === 0 || self::isWordBoundary($candidate[$candidateIdx - 1])) {
|
||||
$firstCharFound = true;
|
||||
} else {
|
||||
// First char not at word boundary, skip it
|
||||
$candidateIdx++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($firstMatchIdx === null) {
|
||||
$firstMatchIdx = $candidateIdx;
|
||||
}
|
||||
|
||||
// Track consecutive matches
|
||||
if ($candidateIdx === $lastMatchIdx + 1) {
|
||||
$consecutiveMatches++;
|
||||
}
|
||||
|
||||
$lastMatchIdx = $candidateIdx;
|
||||
$searchIdx++;
|
||||
}
|
||||
$candidateIdx++;
|
||||
}
|
||||
|
||||
// Did we match all search characters, and did the first char match at a word boundary?
|
||||
if ($searchIdx < $searchLen || !$firstCharFound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Score: position of first match + distance between matches - consecutive bonus
|
||||
// Lower score is better
|
||||
$score = 100 + $firstMatchIdx + ($lastMatchIdx - $firstMatchIdx) - ($consecutiveMatches * 10);
|
||||
|
||||
return $score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a character is a word boundary.
|
||||
*
|
||||
* Word boundaries include: underscore, space, dash, slash, backslash, and other non-alphanumeric chars.
|
||||
*/
|
||||
private static function isWordBoundary(string $char): bool
|
||||
{
|
||||
return !\ctype_alnum($char);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if search string matches candidate (fuzzy).
|
||||
*
|
||||
* @return bool True if all characters in search appear in order in candidate
|
||||
*/
|
||||
public static function matches(string $search, string $candidate): bool
|
||||
{
|
||||
return self::match($search, $candidate) !== null;
|
||||
}
|
||||
}
|
||||
28
vendor/psy/psysh/src/Completion/Refiner/AnalysisRefinerInterface.php
vendored
Normal file
28
vendor/psy/psysh/src/Completion/Refiner/AnalysisRefinerInterface.php
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion\Refiner;
|
||||
|
||||
use Psy\Completion\AnalysisResult;
|
||||
|
||||
/**
|
||||
* Narrows parser output into a more useful completion lane.
|
||||
*
|
||||
* Refiners handle context decisions that depend on more than the parser's
|
||||
* immediate syntactic view of the input.
|
||||
*/
|
||||
interface AnalysisRefinerInterface
|
||||
{
|
||||
/**
|
||||
* Refine the analysis result before sources are queried.
|
||||
*/
|
||||
public function refine(AnalysisResult $analysis): AnalysisResult;
|
||||
}
|
||||
74
vendor/psy/psysh/src/Completion/Refiner/CommandContextRefiner.php
vendored
Normal file
74
vendor/psy/psysh/src/Completion/Refiner/CommandContextRefiner.php
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion\Refiner;
|
||||
|
||||
use Psy\Command\Command;
|
||||
use Psy\CommandArgumentCompletionAware;
|
||||
use Psy\CommandAware;
|
||||
use Psy\CommandMapTrait;
|
||||
use Psy\Completion\AnalysisResult;
|
||||
use Psy\Completion\CompletionKind;
|
||||
|
||||
/**
|
||||
* Hands command tails to command-owned completion once shell syntax is known.
|
||||
*
|
||||
* It resolves which command owns the current tail so argument completion can
|
||||
* follow command-specific rules and vocabulary.
|
||||
*/
|
||||
class CommandContextRefiner implements AnalysisRefinerInterface, CommandAware
|
||||
{
|
||||
use CommandMapTrait;
|
||||
|
||||
/**
|
||||
* @param Command[] $commands Array of PsySH commands
|
||||
*/
|
||||
public function __construct(array $commands)
|
||||
{
|
||||
$this->setCommands($commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function refine(AnalysisResult $analysis): AnalysisResult
|
||||
{
|
||||
if (!$this->supportsRefinement($analysis)) {
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
if (!\preg_match('/^\s*([^\s]+)(\s+.*)$/s', $analysis->input, $matches)) {
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
$commandName = $matches[1];
|
||||
$command = $this->commandMap[$commandName] ?? null;
|
||||
|
||||
if (!$command instanceof CommandArgumentCompletionAware) {
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
if (!$command->supportsArgumentCompletion($analysis)) {
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
return $analysis->withContext(CompletionKind::COMMAND_ARGUMENT, $analysis->prefix, $commandName);
|
||||
}
|
||||
|
||||
private function supportsRefinement(AnalysisResult $analysis): bool
|
||||
{
|
||||
if (($analysis->kinds & CompletionKind::COMMAND_OPTION) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($analysis->kinds & CompletionKind::COMMAND_ELIGIBLE) !== 0;
|
||||
}
|
||||
}
|
||||
53
vendor/psy/psysh/src/Completion/Refiner/CommandSyntaxRefiner.php
vendored
Normal file
53
vendor/psy/psysh/src/Completion/Refiner/CommandSyntaxRefiner.php
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion\Refiner;
|
||||
|
||||
use Psy\Completion\AnalysisResult;
|
||||
use Psy\Completion\CompletionKind;
|
||||
|
||||
/**
|
||||
* Recognizes shell-shaped command input before generic code sources run.
|
||||
*
|
||||
* It classifies bare command heads and option tokens so the rest of the
|
||||
* pipeline can treat shell commands as their own completion mode.
|
||||
*/
|
||||
class CommandSyntaxRefiner implements AnalysisRefinerInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function refine(AnalysisResult $analysis): AnalysisResult
|
||||
{
|
||||
if (($analysis->kinds & CompletionKind::COMMAND_ELIGIBLE) === 0) {
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
$trimmed = \rtrim($analysis->input);
|
||||
|
||||
if (\preg_match('/^([a-z][a-z0-9-]*)\s+.*?(-{1,2}[\w-]*)$/', $trimmed, $matches)) {
|
||||
return $analysis->withContext(CompletionKind::COMMAND_OPTION, $matches[2], $matches[1]);
|
||||
}
|
||||
|
||||
if ($analysis->input !== $trimmed) {
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
if (!\preg_match('/^([a-z][a-z0-9-]*)$/', $trimmed, $matches)) {
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
return $analysis->withContext(
|
||||
CompletionKind::COMMAND | CompletionKind::KEYWORD | CompletionKind::SYMBOL,
|
||||
$matches[1]
|
||||
);
|
||||
}
|
||||
}
|
||||
355
vendor/psy/psysh/src/Completion/Refiner/PartialInputRefiner.php
vendored
Normal file
355
vendor/psy/psysh/src/Completion/Refiner/PartialInputRefiner.php
vendored
Normal file
@@ -0,0 +1,355 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion\Refiner;
|
||||
|
||||
use Psy\Completion\AnalysisResult;
|
||||
use Psy\Completion\CompletionKind;
|
||||
|
||||
/**
|
||||
* Recovers useful completion lanes when valid PHP syntax is not yet available.
|
||||
*
|
||||
* This refiner keeps completion responsive while the user is still in the
|
||||
* middle of typing an expression the parser cannot fully classify yet.
|
||||
*/
|
||||
class PartialInputRefiner implements AnalysisRefinerInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function refine(AnalysisResult $analysis): AnalysisResult
|
||||
{
|
||||
if (!$analysis->parseSucceeded) {
|
||||
$partial = $this->analyzePartialInput($analysis->input, $analysis->tokens);
|
||||
|
||||
return $analysis->withContext($partial->kinds, $partial->prefix, $partial->leftSide, $partial->leftSideNode);
|
||||
}
|
||||
|
||||
if ($analysis->kinds !== CompletionKind::UNKNOWN) {
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
$partial = $this->analyzePartialInput($analysis->input, $analysis->tokens);
|
||||
|
||||
return $this->shouldPreferPartialAnalysis($partial)
|
||||
? $analysis->withContext($partial->kinds, $partial->prefix, $partial->leftSide, $partial->leftSideNode)
|
||||
: $analysis;
|
||||
}
|
||||
|
||||
private function analyzePartialInput(string $input, array $tokens = []): AnalysisResult
|
||||
{
|
||||
$trimmed = \rtrim($input);
|
||||
$hasTrailingSpace = $input !== $trimmed;
|
||||
|
||||
if (\preg_match('/\bnew\s+([\w\\\\]*)$/i', $input, $matches)) {
|
||||
return new AnalysisResult(CompletionKind::CLASS_NAME, $matches[1]);
|
||||
}
|
||||
|
||||
if (\preg_match('/^.*[;\{\}]\s*(\w+)$/', $trimmed, $matches)) {
|
||||
if (!\preg_match('/(?:->|\?->)\w*$/', $trimmed) && !\preg_match('/::\w*$/', $trimmed)) {
|
||||
if ($hasTrailingSpace) {
|
||||
return new AnalysisResult(CompletionKind::UNKNOWN, '');
|
||||
}
|
||||
|
||||
return new AnalysisResult(CompletionKind::KEYWORD | CompletionKind::SYMBOL, $matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (\preg_match('/(\$\w+)(?:->|\?->)([\w]*)$/', $trimmed, $matches)) {
|
||||
return new AnalysisResult(CompletionKind::OBJECT_MEMBER, $matches[2], $matches[1]);
|
||||
}
|
||||
|
||||
if (\preg_match('/([\w\\\\]*\\\\)$/', $trimmed, $matches)) {
|
||||
return new AnalysisResult(CompletionKind::SYMBOL | CompletionKind::NAMESPACE, $matches[1]);
|
||||
}
|
||||
|
||||
if (\preg_match('/([\w\\\\]+)::\$(\w*)$/', $trimmed, $matches)) {
|
||||
return new AnalysisResult(CompletionKind::STATIC_MEMBER, $matches[2], $matches[1]);
|
||||
}
|
||||
|
||||
if (\preg_match('/([\w\\\\]+)::([\w]*)$/', $trimmed, $matches)) {
|
||||
return new AnalysisResult(CompletionKind::STATIC_MEMBER, $matches[2], $matches[1]);
|
||||
}
|
||||
|
||||
$tokenizedObjectMember = $this->analyzeTokenizedObjectMemberAccess($input, $tokens);
|
||||
if ($tokenizedObjectMember !== null) {
|
||||
return $tokenizedObjectMember;
|
||||
}
|
||||
|
||||
if (\preg_match('/\$(\w*)$/', $trimmed, $matches)) {
|
||||
return new AnalysisResult(CompletionKind::VARIABLE, $matches[1]);
|
||||
}
|
||||
|
||||
if (\preg_match('/([\w\\\\]+)$/', $trimmed, $matches)) {
|
||||
if ($hasTrailingSpace) {
|
||||
return new AnalysisResult(CompletionKind::UNKNOWN, '');
|
||||
}
|
||||
|
||||
return new AnalysisResult(CompletionKind::SYMBOL, $matches[1]);
|
||||
}
|
||||
|
||||
return new AnalysisResult(CompletionKind::UNKNOWN, '');
|
||||
}
|
||||
|
||||
private function shouldPreferPartialAnalysis(AnalysisResult $partialAnalysis): bool
|
||||
{
|
||||
return \in_array($partialAnalysis->kinds, [
|
||||
CompletionKind::VARIABLE,
|
||||
CompletionKind::OBJECT_MEMBER,
|
||||
CompletionKind::STATIC_MEMBER,
|
||||
CompletionKind::CLASS_NAME,
|
||||
CompletionKind::SYMBOL | CompletionKind::NAMESPACE,
|
||||
], true);
|
||||
}
|
||||
|
||||
private function analyzeTokenizedObjectMemberAccess(string $input, array $tokens): ?AnalysisResult
|
||||
{
|
||||
$entries = $this->flattenTokens($tokens);
|
||||
if ($entries === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lastIndex = $this->findPreviousSignificantTokenIndex($entries, \count($entries) - 1);
|
||||
if ($lastIndex === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$prefix = '';
|
||||
$operatorIndex = $lastIndex;
|
||||
|
||||
if ($this->isIdentifierToken($entries[$lastIndex])) {
|
||||
$prefix = $entries[$lastIndex]['text'];
|
||||
$operatorIndex = $this->findPreviousSignificantTokenIndex($entries, $lastIndex - 1);
|
||||
if ($operatorIndex === null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->isObjectAccessOperatorToken($entries[$operatorIndex]['token'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$leftEndIndex = $this->findPreviousSignificantTokenIndex($entries, $operatorIndex - 1);
|
||||
if ($leftEndIndex === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$leftStartIndex = $this->findExpressionStartIndex($entries, $leftEndIndex);
|
||||
if ($leftStartIndex === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$leftSide = \mb_substr(
|
||||
$input,
|
||||
$entries[$leftStartIndex]['start'],
|
||||
$entries[$leftEndIndex]['end'] - $entries[$leftStartIndex]['start']
|
||||
);
|
||||
$leftSide = $this->normalizeLeftExpression($leftSide);
|
||||
|
||||
if ($leftSide === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AnalysisResult(CompletionKind::OBJECT_MEMBER, $prefix, $leftSide);
|
||||
}
|
||||
|
||||
private function flattenTokens(array $tokens): array
|
||||
{
|
||||
$entries = [];
|
||||
$position = 0;
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
$text = \is_array($token) ? $token[1] : $token;
|
||||
|
||||
if (\is_array($token) && $token[0] === \T_OPEN_TAG) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$length = \mb_strlen($text);
|
||||
$entries[] = [
|
||||
'token' => $token,
|
||||
'text' => $text,
|
||||
'start' => $position,
|
||||
'end' => $position + $length,
|
||||
];
|
||||
$position += $length;
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
private function findPreviousSignificantTokenIndex(array $entries, int $start): ?int
|
||||
{
|
||||
for ($i = $start; $i >= 0; $i--) {
|
||||
$token = $entries[$i]['token'];
|
||||
|
||||
if (\is_array($token) && \in_array($token[0], [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $i;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function findExpressionStartIndex(array $entries, int $endIndex): ?int
|
||||
{
|
||||
$parenDepth = 0;
|
||||
$bracketDepth = 0;
|
||||
$braceDepth = 0;
|
||||
|
||||
for ($i = $endIndex; $i >= 0; $i--) {
|
||||
$token = $entries[$i]['token'];
|
||||
$text = $entries[$i]['text'];
|
||||
|
||||
if (!\is_array($token)) {
|
||||
if ($text === ')') {
|
||||
$parenDepth++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($text === '(') {
|
||||
if ($parenDepth > 0) {
|
||||
$parenDepth--;
|
||||
continue;
|
||||
}
|
||||
} elseif ($text === ']') {
|
||||
$bracketDepth++;
|
||||
continue;
|
||||
} elseif ($text === '[') {
|
||||
if ($bracketDepth > 0) {
|
||||
$bracketDepth--;
|
||||
continue;
|
||||
}
|
||||
} elseif ($text === '}') {
|
||||
$braceDepth++;
|
||||
continue;
|
||||
} elseif ($text === '{') {
|
||||
if ($braceDepth > 0) {
|
||||
$braceDepth--;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($parenDepth === 0 && $bracketDepth === 0 && $braceDepth === 0 && $this->isExpressionBoundaryToken($token)) {
|
||||
return $this->findNextNonWhitespaceTokenIndex($entries, $i + 1);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($parenDepth === 0 && $bracketDepth === 0 && $braceDepth === 0 && $this->isExpressionBoundaryToken($token)) {
|
||||
return $this->findNextNonWhitespaceTokenIndex($entries, $i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->findNextNonWhitespaceTokenIndex($entries, 0);
|
||||
}
|
||||
|
||||
private function findNextNonWhitespaceTokenIndex(array $entries, int $start): ?int
|
||||
{
|
||||
for ($i = $start; $i < \count($entries); $i++) {
|
||||
$token = $entries[$i]['token'];
|
||||
|
||||
if (\is_array($token) && \in_array($token[0], [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $i;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isExpressionBoundaryToken($token): bool
|
||||
{
|
||||
if (!\is_array($token)) {
|
||||
return \in_array($token, [';', '{', '}', ',', '=', '+', '-', '*', '/', '%', '.', '?', ':', '!', '&', '|', '^', '<', '>'], true);
|
||||
}
|
||||
|
||||
return \in_array($token[0], [
|
||||
\T_DOUBLE_ARROW,
|
||||
\T_BOOLEAN_AND,
|
||||
\T_BOOLEAN_OR,
|
||||
\T_LOGICAL_AND,
|
||||
\T_LOGICAL_OR,
|
||||
\T_LOGICAL_XOR,
|
||||
\T_COALESCE,
|
||||
\T_RETURN,
|
||||
\T_ECHO,
|
||||
\T_PRINT,
|
||||
\T_THROW,
|
||||
], true);
|
||||
}
|
||||
|
||||
private function normalizeLeftExpression(string $expression): string
|
||||
{
|
||||
$expression = \trim($expression);
|
||||
|
||||
while ($this->isWrappedInParentheses($expression)) {
|
||||
$expression = \trim(\substr($expression, 1, -1));
|
||||
}
|
||||
|
||||
return $expression;
|
||||
}
|
||||
|
||||
private function isWrappedInParentheses(string $expression): bool
|
||||
{
|
||||
if (\strlen($expression) < 2 || $expression[0] !== '(' || \substr($expression, -1) !== ')') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$depth = 0;
|
||||
$length = \strlen($expression);
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
if ($expression[$i] === '(') {
|
||||
$depth++;
|
||||
} elseif ($expression[$i] === ')') {
|
||||
$depth--;
|
||||
}
|
||||
|
||||
if ($depth === 0 && $i < $length - 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $depth === 0;
|
||||
}
|
||||
|
||||
private function isIdentifierToken(array $entry): bool
|
||||
{
|
||||
if (!\is_array($entry['token'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \in_array($entry['token'][0], [
|
||||
\T_STRING,
|
||||
\defined('T_NAME_QUALIFIED') ? \T_NAME_QUALIFIED : \T_STRING,
|
||||
\defined('T_NAME_FULLY_QUALIFIED') ? \T_NAME_FULLY_QUALIFIED : \T_STRING,
|
||||
\defined('T_NAME_RELATIVE') ? \T_NAME_RELATIVE : \T_STRING,
|
||||
], true);
|
||||
}
|
||||
|
||||
private function isObjectAccessOperatorToken($token): bool
|
||||
{
|
||||
if (!\is_array($token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($token[0] === \T_OBJECT_OPERATOR) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return \defined('T_NULLSAFE_OBJECT_OPERATOR') && $token[0] === \T_NULLSAFE_OBJECT_OPERATOR;
|
||||
}
|
||||
}
|
||||
79
vendor/psy/psysh/src/Completion/Source/CatalogSource.php
vendored
Normal file
79
vendor/psy/psysh/src/Completion/Source/CatalogSource.php
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion\Source;
|
||||
|
||||
use Psy\Completion\AnalysisResult;
|
||||
use Psy\Completion\SymbolCatalog;
|
||||
|
||||
/**
|
||||
* Catalog-backed completion source.
|
||||
*
|
||||
* Provides completions for symbol types backed by SymbolCatalog (classes,
|
||||
* interfaces, traits, functions, constants).
|
||||
*/
|
||||
class CatalogSource implements SourceInterface
|
||||
{
|
||||
private int $kind;
|
||||
private SymbolCatalog $catalog;
|
||||
|
||||
/** @var callable */
|
||||
private $catalogMethod;
|
||||
|
||||
/**
|
||||
* @param int $kind CompletionKind bitmask to match
|
||||
* @param callable $catalogMethod Callable that takes a SymbolCatalog and returns string[]
|
||||
*/
|
||||
public function __construct(int $kind, callable $catalogMethod, ?SymbolCatalog $catalog = null)
|
||||
{
|
||||
$this->kind = $kind;
|
||||
$this->catalogMethod = $catalogMethod;
|
||||
$this->catalog = $catalog ?? new SymbolCatalog();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function appliesToKind(int $kinds): bool
|
||||
{
|
||||
return ($kinds & $this->kind) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCompletions(AnalysisResult $analysis): array
|
||||
{
|
||||
$prefix = $analysis->prefix;
|
||||
$all = ($this->catalogMethod)($this->catalog);
|
||||
|
||||
// If we have a prefix, try prefix matching first
|
||||
if ($prefix !== '') {
|
||||
$lowerPrefix = \strtolower($prefix);
|
||||
$matches = [];
|
||||
|
||||
foreach ($all as $name) {
|
||||
if (\strpos(\strtolower($name), $lowerPrefix) === 0) {
|
||||
$matches[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
// If we found enough prefix matches, return those (sorted)
|
||||
if (\count($matches) >= 10) {
|
||||
\sort($matches);
|
||||
|
||||
return $matches;
|
||||
}
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
}
|
||||
59
vendor/psy/psysh/src/Completion/Source/ClassConstantSource.php
vendored
Normal file
59
vendor/psy/psysh/src/Completion/Source/ClassConstantSource.php
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion\Source;
|
||||
|
||||
use Psy\Completion\AnalysisResult;
|
||||
use Psy\Completion\CompletionKind;
|
||||
|
||||
/**
|
||||
* Class constant completion source.
|
||||
*
|
||||
* Provides completions for class constants using reflection.
|
||||
* Handles Class::CONSTANT, self::CONSTANT, parent::CONSTANT, static::CONSTANT.
|
||||
*/
|
||||
class ClassConstantSource implements SourceInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function appliesToKind(int $kinds): bool
|
||||
{
|
||||
return ($kinds & CompletionKind::CLASS_CONSTANT) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCompletions(AnalysisResult $analysis): array
|
||||
{
|
||||
if (empty($analysis->leftSideTypes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$constants = [];
|
||||
foreach ($analysis->leftSideTypes as $type) {
|
||||
try {
|
||||
$reflection = new \ReflectionClass($type);
|
||||
} catch (\ReflectionException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($reflection->getReflectionConstants() as $constant) {
|
||||
if ($constant->isPublic()) {
|
||||
$constants[] = $constant->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return \array_values(\array_unique($constants));
|
||||
}
|
||||
}
|
||||
62
vendor/psy/psysh/src/Completion/Source/CommandArgumentSource.php
vendored
Normal file
62
vendor/psy/psysh/src/Completion/Source/CommandArgumentSource.php
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion\Source;
|
||||
|
||||
use Psy\Command\Command;
|
||||
use Psy\CommandArgumentCompletionAware;
|
||||
use Psy\CommandAware;
|
||||
use Psy\CommandMapTrait;
|
||||
use Psy\Completion\AnalysisResult;
|
||||
use Psy\Completion\CompletionKind;
|
||||
|
||||
/**
|
||||
* Command positional argument completion source.
|
||||
*
|
||||
* Delegates argument-tail completion to commands that explicitly opt in.
|
||||
*/
|
||||
class CommandArgumentSource implements CommandAware, SourceInterface
|
||||
{
|
||||
use CommandMapTrait;
|
||||
|
||||
/**
|
||||
* @param Command[] $commands Array of PsySH commands
|
||||
*/
|
||||
public function __construct(array $commands)
|
||||
{
|
||||
$this->setCommands($commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function appliesToKind(int $kinds): bool
|
||||
{
|
||||
return ($kinds & CompletionKind::COMMAND_ARGUMENT) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCompletions(AnalysisResult $analysis): array
|
||||
{
|
||||
$commandName = \is_string($analysis->leftSide) ? $analysis->leftSide : null;
|
||||
$command = $commandName !== null ? ($this->commandMap[$commandName] ?? null) : null;
|
||||
|
||||
if (!$command instanceof CommandArgumentCompletionAware) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// CommandContextRefiner already verified supportsArgumentCompletion()
|
||||
// before setting COMMAND_ARGUMENT kind.
|
||||
return $command->getArgumentCompletions($analysis);
|
||||
}
|
||||
}
|
||||
123
vendor/psy/psysh/src/Completion/Source/CommandOptionSource.php
vendored
Normal file
123
vendor/psy/psysh/src/Completion/Source/CommandOptionSource.php
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion\Source;
|
||||
|
||||
use Psy\Command\Command;
|
||||
use Psy\CommandAware;
|
||||
use Psy\CommandMapTrait;
|
||||
use Psy\Completion\AnalysisResult;
|
||||
use Psy\Completion\CompletionKind;
|
||||
use Symfony\Component\Console\Input\StringInput;
|
||||
|
||||
/**
|
||||
* Command option/argument completion source.
|
||||
*
|
||||
* Provides completions for PsySH command options (e.g., --option, -o) and arguments.
|
||||
*/
|
||||
class CommandOptionSource implements CommandAware, SourceInterface
|
||||
{
|
||||
use CommandMapTrait;
|
||||
|
||||
/**
|
||||
* @param Command[] $commands Array of PsySH commands
|
||||
*/
|
||||
public function __construct(array $commands)
|
||||
{
|
||||
$this->setCommands($commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function appliesToKind(int $kinds): bool
|
||||
{
|
||||
return ($kinds & CompletionKind::COMMAND_OPTION) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCompletions(AnalysisResult $analysis): array
|
||||
{
|
||||
$commandName = $analysis->leftSide;
|
||||
if (!\is_string($commandName) || !isset($this->commandMap[$commandName])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$command = $this->commandMap[$commandName];
|
||||
$input = $this->createInput($analysis);
|
||||
$matches = [];
|
||||
|
||||
foreach ($command->getDefinition()->getOptions() as $option) {
|
||||
$longName = '--'.$option->getName();
|
||||
$shortName = $option->getShortcut() !== null ? '-'.$option->getShortcut() : null;
|
||||
|
||||
if (!$option->isArray() && $this->isOptionUsed($input, $analysis, $longName, $shortName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matches[] = $longName;
|
||||
|
||||
if ($shortName !== null) {
|
||||
$matches[] = $shortName;
|
||||
}
|
||||
}
|
||||
|
||||
\sort($matches);
|
||||
|
||||
return $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a StringInput for token scanning, or null if input is empty/unparseable.
|
||||
*/
|
||||
private function createInput(AnalysisResult $analysis): ?StringInput
|
||||
{
|
||||
if ($analysis->input === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new StringInput($analysis->input);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an option has already been used in the input.
|
||||
*/
|
||||
private function isOptionUsed(?StringInput $input, AnalysisResult $analysis, string $longName, ?string $shortName): bool
|
||||
{
|
||||
if ($input === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$names = [$longName];
|
||||
if ($shortName !== null) {
|
||||
$names[] = $shortName;
|
||||
}
|
||||
|
||||
if ($input->hasParameterOption($names, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// hasParameterOption handles --long, --long=val, and -o as the
|
||||
// first short option in a group, but not combined short options
|
||||
// like -l in -al. Fall back to a simple string match for that.
|
||||
if ($shortName !== null && \preg_match('/(^|\s)-\w*'.\preg_quote($shortName[1], '/').'/', $analysis->input)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
71
vendor/psy/psysh/src/Completion/Source/CommandSource.php
vendored
Normal file
71
vendor/psy/psysh/src/Completion/Source/CommandSource.php
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion\Source;
|
||||
|
||||
use Psy\Command\Command;
|
||||
use Psy\CommandAware;
|
||||
use Psy\Completion\AnalysisResult;
|
||||
use Psy\Completion\CompletionKind;
|
||||
|
||||
/**
|
||||
* PsySH command completion source.
|
||||
*
|
||||
* Provides completions for PsySH commands (e.g., ls, doc, show).
|
||||
*/
|
||||
class CommandSource implements CommandAware, SourceInterface
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $commandNames = [];
|
||||
|
||||
/**
|
||||
* @param Command[] $commands Array of PsySH commands
|
||||
*/
|
||||
public function __construct(array $commands)
|
||||
{
|
||||
$this->setCommands($commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set commands for completion.
|
||||
*
|
||||
* @param Command[] $commands
|
||||
*/
|
||||
public function setCommands(array $commands): void
|
||||
{
|
||||
$names = [];
|
||||
foreach ($commands as $command) {
|
||||
$names[] = $command->getName();
|
||||
foreach ($command->getAliases() as $alias) {
|
||||
$names[] = $alias;
|
||||
}
|
||||
}
|
||||
|
||||
\sort($names);
|
||||
$this->commandNames = $names;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function appliesToKind(int $kinds): bool
|
||||
{
|
||||
return ($kinds & CompletionKind::COMMAND) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCompletions(AnalysisResult $analysis): array
|
||||
{
|
||||
return $this->commandNames;
|
||||
}
|
||||
}
|
||||
54
vendor/psy/psysh/src/Completion/Source/HistorySource.php
vendored
Normal file
54
vendor/psy/psysh/src/Completion/Source/HistorySource.php
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion\Source;
|
||||
|
||||
use Psy\Completion\AnalysisResult;
|
||||
use Psy\Completion\CompletionKind;
|
||||
use Psy\Readline\Interactive\Input\History;
|
||||
|
||||
/**
|
||||
* History-based completion source.
|
||||
*
|
||||
* Provides completion candidates from command history at the start of input.
|
||||
*/
|
||||
class HistorySource implements SourceInterface
|
||||
{
|
||||
private History $history;
|
||||
|
||||
public function __construct(History $history)
|
||||
{
|
||||
$this->history = $history;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function appliesToKind(int $kinds): bool
|
||||
{
|
||||
return ($kinds & CompletionKind::COMMAND) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCompletions(AnalysisResult $analysis): array
|
||||
{
|
||||
if ($analysis->prefix === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Newest first, deduplicated
|
||||
$commands = $this->history->search('', false);
|
||||
|
||||
return \array_values(\array_unique($commands));
|
||||
}
|
||||
}
|
||||
69
vendor/psy/psysh/src/Completion/Source/KeywordSource.php
vendored
Normal file
69
vendor/psy/psysh/src/Completion/Source/KeywordSource.php
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Completion\Source;
|
||||
|
||||
use Psy\Completion\AnalysisResult;
|
||||
use Psy\Completion\CompletionKind;
|
||||
|
||||
/**
|
||||
* PHP keyword completion source.
|
||||
*
|
||||
* Provides completions for function-like PHP keywords (echo, isset, etc.).
|
||||
*/
|
||||
class KeywordSource implements SourceInterface
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $keywords = [
|
||||
'array',
|
||||
'clone',
|
||||
'declare',
|
||||
'die',
|
||||
'echo',
|
||||
'empty',
|
||||
'eval',
|
||||
'exit',
|
||||
'fn',
|
||||
'include',
|
||||
'include_once',
|
||||
'isset',
|
||||
'list',
|
||||
'print',
|
||||
'require',
|
||||
'require_once',
|
||||
'unset',
|
||||
'yield',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (\PHP_VERSION_ID >= 80000) {
|
||||
$this->keywords[] = 'match';
|
||||
\sort($this->keywords);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function appliesToKind(int $kinds): bool
|
||||
{
|
||||
return ($kinds & CompletionKind::KEYWORD) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCompletions(AnalysisResult $analysis): array
|
||||
{
|
||||
return $this->keywords;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user