update lock clucknut
All checks were successful
All checks were successful
This commit is contained in:
2
vendor/psy/psysh/LICENSE
vendored
2
vendor/psy/psysh/LICENSE
vendored
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012-2025 Justin Hileman
|
||||
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
|
||||
|
||||
159
vendor/psy/psysh/bin/fetch-manual
vendored
159
vendor/psy/psysh/bin/fetch-manual
vendored
@@ -1,159 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
// Fetch the latest manual from GitHub releases for bundling in PHAR builds
|
||||
|
||||
const RELEASES_URL = 'https://api.github.com/repos/bobthecow/psysh-manual/releases';
|
||||
|
||||
function fetchLatestManual(): bool {
|
||||
echo "Fetching latest manual release info...\n";
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'user_agent' => 'PsySH Manual Fetcher',
|
||||
'timeout' => 10.0,
|
||||
],
|
||||
]);
|
||||
|
||||
$result = @file_get_contents(RELEASES_URL, false, $context);
|
||||
if (!$result) {
|
||||
echo "Failed to fetch releases from GitHub\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
$releases = json_decode($result, true);
|
||||
if (!$releases || !is_array($releases)) {
|
||||
echo "Invalid response from GitHub releases API\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the first release with a manifest
|
||||
foreach ($releases as $release) {
|
||||
$manifest = fetchManifest($release, $context);
|
||||
if ($manifest === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find English PHP format manual in the manifest
|
||||
foreach ($manifest['manuals'] as $manual) {
|
||||
if ($manual['lang'] === 'en' && $manual['format'] === 'php') {
|
||||
echo "Found manual v{$manual['version']} (en, php format)\n";
|
||||
|
||||
// Find the download URL for the manual
|
||||
$filename = sprintf('psysh-manual-v%s-en.tar.gz', $manual['version']);
|
||||
$downloadUrl = null;
|
||||
|
||||
foreach ($release['assets'] as $asset) {
|
||||
if ($asset['name'] === $filename) {
|
||||
$downloadUrl = $asset['browser_download_url'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($downloadUrl === null) {
|
||||
echo "Could not find download URL for $filename\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Download and extract the manual
|
||||
return downloadAndExtractManual($downloadUrl, $filename, $context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "No English PHP manual found in releases\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
function fetchManifest(array $release, $context): ?array {
|
||||
foreach ($release['assets'] as $asset) {
|
||||
if ($asset['name'] === 'manifest.json') {
|
||||
$manifestContent = @file_get_contents($asset['browser_download_url'], false, $context);
|
||||
if ($manifestContent) {
|
||||
return json_decode($manifestContent, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function downloadAndExtractManual(string $downloadUrl, string $filename, $context): bool {
|
||||
echo "Downloading $filename...\n";
|
||||
|
||||
$tempFile = sys_get_temp_dir() . '/' . $filename;
|
||||
$content = @file_get_contents($downloadUrl, false, $context);
|
||||
|
||||
if (!$content) {
|
||||
echo "Failed to download manual\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file_put_contents($tempFile, $content)) {
|
||||
echo "Failed to save manual to $tempFile\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
echo "Extracting manual...\n";
|
||||
|
||||
// Create temp directory for extraction
|
||||
$tempDir = sys_get_temp_dir() . '/psysh-manual-' . uniqid();
|
||||
if (!mkdir($tempDir)) {
|
||||
echo "Failed to create temp directory\n";
|
||||
unlink($tempFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Extract using PharData
|
||||
$phar = new PharData($tempFile);
|
||||
$phar->extractTo($tempDir);
|
||||
|
||||
// Find the php_manual.php file
|
||||
$extractedFile = $tempDir . '/php_manual.php';
|
||||
if (!file_exists($extractedFile)) {
|
||||
echo "php_manual.php not found in extracted archive\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Copy to current directory
|
||||
if (!copy($extractedFile, 'php_manual.php')) {
|
||||
echo "Failed to copy manual to current directory\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
echo "Successfully fetched manual to php_manual.php\n";
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
echo "Failed to extract manual: " . $e->getMessage() . "\n";
|
||||
return false;
|
||||
} finally {
|
||||
// Clean up
|
||||
@unlink($tempFile);
|
||||
removeDirectory($tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
function removeDirectory(string $dir) {
|
||||
if (!is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = array_diff(scandir($dir), ['.', '..']);
|
||||
foreach ($files as $file) {
|
||||
$path = $dir . '/' . $file;
|
||||
is_dir($path) ? removeDirectory($path) : unlink($path);
|
||||
}
|
||||
rmdir($dir);
|
||||
}
|
||||
|
||||
// Main execution
|
||||
exit(fetchLatestManual() ? 0 : 1);
|
||||
243
vendor/psy/psysh/bin/psysh
vendored
Normal file → Executable file
243
vendor/psy/psysh/bin/psysh
vendored
Normal file → Executable file
@@ -4,7 +4,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -14,6 +14,9 @@
|
||||
// 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();
|
||||
@@ -24,22 +27,223 @@ call_user_func(function () {
|
||||
exit(1);
|
||||
}
|
||||
$cwd = $argv[$i + 1];
|
||||
break;
|
||||
$cwdFromArg = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^--cwd=/', $arg)) {
|
||||
$cwd = substr($arg, 6);
|
||||
break;
|
||||
$cwdFromArg = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($arg === '--trust-project') {
|
||||
$forceTrust = true;
|
||||
$forceUntrust = false;
|
||||
} elseif ($arg === '--no-trust-project') {
|
||||
$forceUntrust = true;
|
||||
$forceTrust = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Or fall back to the actual cwd
|
||||
if (!isset($cwd)) {
|
||||
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);
|
||||
@@ -54,7 +258,18 @@ call_user_func(function () {
|
||||
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')) {
|
||||
if (realpath($path) !== realpath(__DIR__ . '/..')) {
|
||||
$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);
|
||||
}
|
||||
|
||||
@@ -69,11 +284,23 @@ call_user_func(function () {
|
||||
// Or a composer.lock
|
||||
if (is_file($path . '/composer.lock')) {
|
||||
if ($cfg = json_decode(file_get_contents($path . '/composer.lock'), true)) {
|
||||
foreach (array_merge($cfg['packages'], $cfg['packages-dev']) as $pkg) {
|
||||
$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')) {
|
||||
if (realpath($path . '/vendor') !== realpath(__DIR__ . '/../../..')) {
|
||||
$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);
|
||||
}
|
||||
|
||||
|
||||
37
vendor/psy/psysh/build/composer.json
vendored
37
vendor/psy/psysh/build/composer.json
vendored
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "psy/psysh-build",
|
||||
"description": "Build configuration for PsySH phar distribution.",
|
||||
"type": "project",
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.0 || ^7.4",
|
||||
"ext-json": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"composer/class-map-generator": "^1.0",
|
||||
"nikic/php-parser": "^5.0 || ^4.0",
|
||||
"symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4",
|
||||
"symfony/polyfill-iconv": "^1.0",
|
||||
"symfony/polyfill-mbstring": "^1.0",
|
||||
"symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"roave/security-advisories": "dev-latest"
|
||||
},
|
||||
"autoload": {
|
||||
"files": ["src/functions.php"],
|
||||
"psr-4": {
|
||||
"Psy\\": "src/"
|
||||
}
|
||||
},
|
||||
"bin": ["bin/psysh"],
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.4.99"
|
||||
},
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4"
|
||||
}
|
||||
}
|
||||
2349
vendor/psy/psysh/build/composer.lock
generated
vendored
2349
vendor/psy/psysh/build/composer.lock
generated
vendored
File diff suppressed because it is too large
Load Diff
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);
|
||||
}
|
||||
}
|
||||
185
vendor/psy/psysh/src/CodeCleaner.php
vendored
185
vendor/psy/psysh/src/CodeCleaner.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -11,11 +11,14 @@
|
||||
|
||||
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;
|
||||
@@ -67,6 +70,7 @@ class CodeCleaner
|
||||
private ?array $namespace = null;
|
||||
private array $messages = [];
|
||||
private array $aliasesByNamespace = [];
|
||||
private array $aliasesByTypeByNamespace = [];
|
||||
|
||||
/**
|
||||
* CodeCleaner constructor.
|
||||
@@ -323,6 +327,7 @@ public function setAliasesForNamespace(?Name $namespace, array $aliases)
|
||||
{
|
||||
$namespaceKey = \strtolower($namespace ? $namespace->toString() : '');
|
||||
$this->aliasesByNamespace[$namespaceKey] = $aliases;
|
||||
$this->aliasesByTypeByNamespace[$namespaceKey][Use_::TYPE_NORMAL] = $aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -340,7 +345,43 @@ public function getAliasesForNamespace($namespace): array
|
||||
$namespaceName = $namespace instanceof Name ? $namespace->toString() : $namespace;
|
||||
$namespaceKey = \strtolower($namespaceName ?? '');
|
||||
|
||||
return $this->aliasesByNamespace[$namespaceKey] ?? [];
|
||||
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]];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -437,6 +478,134 @@ public function resolveClassName(string $name): string
|
||||
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.
|
||||
*
|
||||
@@ -471,7 +640,7 @@ protected function parse(string $code, bool $requireSemicolons = false)
|
||||
{
|
||||
try {
|
||||
return $this->parser->parse($code);
|
||||
} catch (\PhpParser\Error $e) {
|
||||
} catch (PhpParserError $e) {
|
||||
if ($this->parseErrorIsUnclosedString($e, $code)) {
|
||||
return false;
|
||||
}
|
||||
@@ -495,13 +664,13 @@ protected function parse(string $code, bool $requireSemicolons = false)
|
||||
try {
|
||||
// Unexpected EOF, try again with an implicit semicolon
|
||||
return $this->parser->parse($code.';');
|
||||
} catch (\PhpParser\Error $e) {
|
||||
} catch (PhpParserError $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function parseErrorIsEOF(\PhpParser\Error $e): bool
|
||||
private function parseErrorIsEOF(PhpParserError $e): bool
|
||||
{
|
||||
$msg = $e->getRawMessage();
|
||||
|
||||
@@ -515,7 +684,7 @@ private function parseErrorIsEOF(\PhpParser\Error $e): bool
|
||||
* their own special beautiful snowflake syntax error just for
|
||||
* themselves.
|
||||
*/
|
||||
private function parseErrorIsUnclosedString(\PhpParser\Error $e, string $code): bool
|
||||
private function parseErrorIsUnclosedString(PhpParserError $e, string $code): bool
|
||||
{
|
||||
if ($e->getRawMessage() !== 'Syntax error, unexpected T_ENCAPSED_AND_WHITESPACE') {
|
||||
return false;
|
||||
@@ -530,12 +699,12 @@ private function parseErrorIsUnclosedString(\PhpParser\Error $e, string $code):
|
||||
return true;
|
||||
}
|
||||
|
||||
private function parseErrorIsUnterminatedComment(\PhpParser\Error $e, string $code): bool
|
||||
private function parseErrorIsUnterminatedComment(PhpParserError $e, string $code): bool
|
||||
{
|
||||
return $e->getRawMessage() === 'Unterminated comment';
|
||||
}
|
||||
|
||||
private function parseErrorIsTrailingComma(\PhpParser\Error $e, string $code): bool
|
||||
private function parseErrorIsTrailingComma(PhpParserError $e, string $code): bool
|
||||
{
|
||||
return ($e->getRawMessage() === 'A trailing comma is not allowed here') && (\substr(\rtrim($code), -1) === ',');
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -14,6 +14,7 @@
|
||||
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;
|
||||
@@ -74,14 +75,14 @@ private function addImplicitReturn(array $nodes): array
|
||||
$case->stmts[] = $caseLast;
|
||||
}
|
||||
}
|
||||
} elseif ($last instanceof Expr && !($last instanceof Exit_)) {
|
||||
} 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_)) {
|
||||
} 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(),
|
||||
@@ -100,7 +101,7 @@ private function addImplicitReturn(array $nodes): array
|
||||
// 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)) {
|
||||
if (self::isNonExpressionStmt($last) && !self::isThrowNode($last)) {
|
||||
$nodes[] = new Return_(NoReturnValue::create());
|
||||
}
|
||||
|
||||
@@ -122,4 +123,13 @@ private static function isNonExpressionStmt(Node $node): bool
|
||||
!$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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -267,11 +267,11 @@ private function buildShortNameMap(): void
|
||||
{
|
||||
$this->shortNameMap = [];
|
||||
|
||||
$allClasses = \array_merge(
|
||||
\get_declared_classes(),
|
||||
\get_declared_interfaces(),
|
||||
\get_declared_traits()
|
||||
);
|
||||
$allClasses = [
|
||||
...\get_declared_classes(),
|
||||
...\get_declared_interfaces(),
|
||||
...\get_declared_traits(),
|
||||
];
|
||||
|
||||
// First pass: collect all matching classes
|
||||
$candidatesByShortName = [];
|
||||
@@ -342,9 +342,7 @@ private function shouldIncludeClass(string $fqn): bool
|
||||
*/
|
||||
private function normalizeNamespaces(array $namespaces): array
|
||||
{
|
||||
return \array_map(function ($namespace) {
|
||||
return \trim($namespace, '\\').'\\';
|
||||
}, $namespaces);
|
||||
return \array_map(fn ($namespace) => \trim($namespace, '\\').'\\', $namespaces);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -29,6 +29,7 @@ abstract class NamespaceAwarePass extends CodeCleanerPass
|
||||
protected array $namespace = [];
|
||||
protected array $currentScope = [];
|
||||
protected array $aliases = [];
|
||||
protected array $aliasesByType = [];
|
||||
protected ?CodeCleaner $cleaner = null;
|
||||
|
||||
/**
|
||||
@@ -51,6 +52,7 @@ public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->namespace = [];
|
||||
$this->currentScope = [];
|
||||
$this->aliasesByType = [];
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -71,23 +73,25 @@ public function enterNode(Node $node)
|
||||
// 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->aliases = $this->cleaner->getAliasesForNamespace($node->name);
|
||||
$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->aliases[\strtolower($useItem->getAlias())] = $useItem->name;
|
||||
$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->aliases[\strtolower($useItem->getAlias())] = Name::concat($node->prefix, $useItem->name);
|
||||
$this->setAliasForType(\strtolower($useItem->getAlias()), Name::concat($node->prefix, $useItem->name), $this->getUseImportType($node, $useItem));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,14 +112,17 @@ public function enterNode(Node $node)
|
||||
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->setAliasesForNamespace($node->name, $this->aliases);
|
||||
$this->cleaner->setAliasesByTypeForNamespace($node->name, $this->aliasesByType);
|
||||
}
|
||||
}
|
||||
|
||||
$this->aliases = [];
|
||||
$this->aliasesByType = [];
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -130,6 +137,8 @@ public function leaveNode(Node $node)
|
||||
*/
|
||||
protected function getFullyQualifiedName($name): string
|
||||
{
|
||||
$this->syncCompatAliases();
|
||||
|
||||
if ($name instanceof FullyQualifiedName) {
|
||||
return \implode('\\', $this->getParts($name));
|
||||
}
|
||||
@@ -166,4 +175,54 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -106,7 +106,7 @@ private function setNamespace(?Name $namespace)
|
||||
$this->cleaner->setNamespace($namespace);
|
||||
|
||||
// Always clear aliases when changing namespace
|
||||
$this->cleaner->setAliasesForNamespace($namespace, []);
|
||||
$this->cleaner->setAliasesByTypeForNamespace($namespace, []);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Identifier;
|
||||
use PhpParser\Node\Name; // @phan-suppress-current-line PhanUnreferencedUseNormal - used for type checks
|
||||
use PhpParser\Node\Stmt\Namespace_;
|
||||
use PhpParser\Node\Stmt\Use_;
|
||||
use PhpParser\Node\Stmt\UseItem;
|
||||
@@ -70,9 +69,9 @@ public function beforeTraverse(array $nodes)
|
||||
// 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')) {
|
||||
$aliases = $this->cleaner->getAliasesForNamespace($node->name);
|
||||
if (!empty($aliases)) {
|
||||
$useStatements = $this->createUseStatements($aliases);
|
||||
$aliasesByType = $this->cleaner->getAliasesByTypeForNamespace($node->name);
|
||||
if (!empty($aliasesByType)) {
|
||||
$useStatements = $this->createUseStatements($aliasesByType);
|
||||
$node->stmts = \array_merge($useStatements, $node->stmts ?? []);
|
||||
}
|
||||
}
|
||||
@@ -84,9 +83,9 @@ public function beforeTraverse(array $nodes)
|
||||
|
||||
// No namespace declaration in input, or re-applied by NamespacePass; re-inject use
|
||||
// statements for the empty namespace.
|
||||
$aliases = $this->cleaner->getAliasesForNamespace(null);
|
||||
if (!empty($aliases)) {
|
||||
$useStatements = $this->createUseStatements($aliases);
|
||||
$aliasesByType = $this->cleaner->getAliasesByTypeForNamespace(null);
|
||||
if (!empty($aliasesByType)) {
|
||||
$useStatements = $this->createUseStatements($aliasesByType);
|
||||
$nodes = \array_merge($useStatements, $nodes);
|
||||
}
|
||||
|
||||
@@ -106,8 +105,8 @@ public function afterTraverse(array $nodes)
|
||||
}
|
||||
|
||||
// Persist aliases if they're at the global level (not inside any namespace)
|
||||
if (!empty($this->aliases)) {
|
||||
$this->cleaner->setAliasesForNamespace(null, $this->aliases);
|
||||
if (!empty($this->aliasesByType)) {
|
||||
$this->cleaner->setAliasesByTypeForNamespace(null, $this->aliasesByType);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -122,32 +121,40 @@ public function afterTraverse(array $nodes)
|
||||
*/
|
||||
private function validateUseStatement(Use_ $stmt): void
|
||||
{
|
||||
$seenAliases = [];
|
||||
|
||||
foreach ($stmt->uses as $useItem) {
|
||||
$alias = \strtolower($useItem->getAlias());
|
||||
$type = $this->getUseImportType($stmt, $useItem);
|
||||
|
||||
if (isset($this->aliases[$alias])) {
|
||||
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 $aliases Map of lowercase alias names to Name nodes
|
||||
* @param array $aliasesByType Map of Use_::TYPE_* constants to alias maps
|
||||
*
|
||||
* @return Use_[] Array of use statement nodes
|
||||
*/
|
||||
private function createUseStatements(array $aliases): array
|
||||
private function createUseStatements(array $aliasesByType): array
|
||||
{
|
||||
$useStatements = [];
|
||||
foreach ($aliases 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], Use_::TYPE_NORMAL, ['psyshReinjected' => true]);
|
||||
|
||||
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;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
2
vendor/psy/psysh/src/CodeCleanerAware.php
vendored
2
vendor/psy/psysh/src/CodeCleanerAware.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
51
vendor/psy/psysh/src/Command/BufferCommand.php
vendored
51
vendor/psy/psysh/src/Command/BufferCommand.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -11,7 +11,10 @@
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use Psy\Output\ShellOutput;
|
||||
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;
|
||||
@@ -21,8 +24,10 @@
|
||||
*
|
||||
* Shows and clears the buffer for the current multi-line expression.
|
||||
*/
|
||||
class BufferCommand extends Command
|
||||
class BufferCommand extends Command implements ReadlineAware
|
||||
{
|
||||
private ?Readline $readline = null;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -52,18 +57,32 @@ protected function configure(): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$shell = $this->getShell();
|
||||
|
||||
$buf = $shell->getCodeBuffer();
|
||||
$shellOutput = $this->shellOutput($output);
|
||||
$readline = $this->getLegacyReadline();
|
||||
$legacyBuffer = $readline->getBuffer();
|
||||
$shellBuffer = $shell->getPendingCodeBuffer();
|
||||
$buf = $legacyBuffer !== [] ? $legacyBuffer : $shellBuffer;
|
||||
if ($input->getOption('clear')) {
|
||||
$shell->resetCodeBuffer();
|
||||
$output->writeln($this->formatLines($buf, 'urgent'), ShellOutput::NUMBER_LINES);
|
||||
$readline->clearBuffer();
|
||||
if ($shellBuffer !== []) {
|
||||
$shell->clearPendingCodeBuffer();
|
||||
}
|
||||
$shellOutput->writeln($this->formatLines($buf, 'urgent'), ShellOutputAdapter::NUMBER_LINES);
|
||||
} else {
|
||||
$output->writeln($this->formatLines($buf), ShellOutput::NUMBER_LINES);
|
||||
$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.
|
||||
*
|
||||
@@ -76,8 +95,18 @@ protected function formatLines(array $lines, string $type = 'return'): array
|
||||
{
|
||||
$template = \sprintf('<%s>%%s</%s>', $type, $type);
|
||||
|
||||
return \array_map(function ($line) use ($template) {
|
||||
return \sprintf($template, $line);
|
||||
}, $lines);
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use PhpParser\Error as PhpParserError;
|
||||
use PhpParser\Parser;
|
||||
use Psy\Exception\ParseErrorException;
|
||||
use Psy\ParserFactory;
|
||||
@@ -42,7 +43,7 @@ public function parse(string $code): array
|
||||
|
||||
try {
|
||||
return $this->parser->parse($code);
|
||||
} catch (\PhpParser\Error $e) {
|
||||
} catch (PhpParserError $e) {
|
||||
if (\strpos($e->getMessage(), 'unexpected EOF') === false) {
|
||||
throw ParseErrorException::fromParseError($e);
|
||||
}
|
||||
@@ -50,7 +51,7 @@ public function parse(string $code): array
|
||||
// If we got an unexpected EOF, let's try it again with a semicolon.
|
||||
try {
|
||||
return $this->parser->parse($code.';');
|
||||
} catch (\PhpParser\Error $_e) {
|
||||
} catch (PhpParserError $_e) {
|
||||
// Throw the original error, not the semicolon one.
|
||||
throw ParseErrorException::fromParseError($e);
|
||||
}
|
||||
|
||||
55
vendor/psy/psysh/src/Command/Command.php
vendored
55
vendor/psy/psysh/src/Command/Command.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -11,11 +11,17 @@
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -52,6 +58,23 @@ protected function getShell(): 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}
|
||||
*/
|
||||
@@ -83,6 +106,14 @@ public function asText(): string
|
||||
return \implode("\n", $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render help text for the current input context.
|
||||
*/
|
||||
public function asTextForInput(InputInterface $input): string
|
||||
{
|
||||
return $this->asText();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -90,9 +121,10 @@ private function getArguments(): array
|
||||
{
|
||||
$hidden = $this->getHiddenArguments();
|
||||
|
||||
return \array_filter($this->getNativeDefinition()->getArguments(), function ($argument) use ($hidden) {
|
||||
return !\in_array($argument->getName(), $hidden);
|
||||
});
|
||||
return \array_filter(
|
||||
$this->getNativeDefinition()->getArguments(),
|
||||
fn ($argument) => !\in_array($argument->getName(), $hidden)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,9 +144,10 @@ private function getOptions(): array
|
||||
{
|
||||
$hidden = $this->getHiddenOptions();
|
||||
|
||||
return \array_filter($this->getNativeDefinition()->getOptions(), function ($option) use ($hidden) {
|
||||
return !\in_array($option->getName(), $hidden);
|
||||
});
|
||||
return \array_filter(
|
||||
$this->getNativeDefinition()->getOptions(),
|
||||
fn ($option) => !\in_array($option->getName(), $hidden)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,4 +304,12 @@ protected function getTable(OutputInterface $output)
|
||||
->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;
|
||||
}
|
||||
}
|
||||
49
vendor/psy/psysh/src/Command/DocCommand.php
vendored
49
vendor/psy/psysh/src/Command/DocCommand.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -17,9 +17,9 @@
|
||||
use Psy\Formatter\SignatureFormatter;
|
||||
use Psy\Input\CodeArgument;
|
||||
use Psy\ManualUpdater\ManualUpdate;
|
||||
use Psy\Output\ShellOutput;
|
||||
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;
|
||||
@@ -85,6 +85,8 @@ protected function configure(): void
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$shellOutput = $this->shellOutput($output);
|
||||
|
||||
if ($input->getOption('update-manual') !== false) {
|
||||
return $this->handleUpdateManual($input, $output);
|
||||
}
|
||||
@@ -104,9 +106,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
|
||||
$hasManual = $this->getShell()->getManual() !== null;
|
||||
|
||||
if ($output instanceof ShellOutput) {
|
||||
$output->startPaging();
|
||||
}
|
||||
$shellOutput->startPaging();
|
||||
|
||||
// Maybe include the declaring class
|
||||
if ($reflector instanceof \ReflectionMethod || $reflector instanceof \ReflectionProperty) {
|
||||
@@ -120,7 +120,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
$output->writeln('<warning>PHP manual not found</warning>');
|
||||
$output->writeln(' To document core PHP functionality, download the PHP reference manual:');
|
||||
$output->writeln(' https://github.com/bobthecow/psysh/wiki/PHP-manual');
|
||||
} else {
|
||||
} elseif ($doc !== null) {
|
||||
$output->writeln($doc);
|
||||
}
|
||||
|
||||
@@ -146,9 +146,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
}
|
||||
}
|
||||
|
||||
if ($output instanceof ShellOutput) {
|
||||
$output->stopPaging();
|
||||
}
|
||||
$shellOutput->stopPaging();
|
||||
|
||||
// Set some magic local variables
|
||||
$this->setCommandScopeVariables($reflector);
|
||||
@@ -324,7 +322,7 @@ private function getManualDocById($id)
|
||||
|
||||
case 3:
|
||||
if ($doc = $manual->get($id)) {
|
||||
$width = $this->getTerminalWidth();
|
||||
$width = Tty::getWidth();
|
||||
$formatter = new ManualFormatter($width, $manual);
|
||||
|
||||
return $formatter->format($doc);
|
||||
@@ -335,35 +333,4 @@ private function getManualDocById($id)
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current terminal width for text wrapping.
|
||||
*
|
||||
* @return int Terminal width in columns
|
||||
*/
|
||||
private function getTerminalWidth(): int
|
||||
{
|
||||
// Query terminal size directly
|
||||
if (\function_exists('shell_exec')) {
|
||||
// Output format: "rows cols"
|
||||
$output = @\shell_exec('stty size </dev/tty 2>/dev/null');
|
||||
if ($output && \preg_match('/^\d+ (\d+)$/', \trim($output), $matches)) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
$width = @\shell_exec('tput cols </dev/tty 2>/dev/null');
|
||||
if ($width && \is_numeric(\trim($width))) {
|
||||
return (int) \trim($width);
|
||||
}
|
||||
}
|
||||
|
||||
// Check COLUMNS environment variable (may be stale after resize)
|
||||
$width = \getenv('COLUMNS');
|
||||
if ($width && \is_numeric(\trim($width))) {
|
||||
return (int) \trim($width);
|
||||
}
|
||||
|
||||
// Fallback to 100 if we can't detect
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
|
||||
10
vendor/psy/psysh/src/Command/DumpCommand.php
vendored
10
vendor/psy/psysh/src/Command/DumpCommand.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -11,9 +11,7 @@
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use Psy\Exception\RuntimeException;
|
||||
use Psy\Input\CodeArgument;
|
||||
use Psy\Output\ShellOutput;
|
||||
use Psy\VarDumper\Presenter;
|
||||
use Psy\VarDumper\PresenterAware;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
@@ -73,13 +71,9 @@ protected function configure(): void
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
if (!$output instanceof ShellOutput) {
|
||||
throw new RuntimeException('DumpCommand requires a ShellOutput');
|
||||
}
|
||||
|
||||
$depth = $input->getOption('depth');
|
||||
$target = $this->resolveCode($input->getArgument('target'));
|
||||
$output->page($this->presenter->present($target, $depth, $input->getOption('all') ? Presenter::VERBOSE : 0));
|
||||
$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));
|
||||
|
||||
37
vendor/psy/psysh/src/Command/EditCommand.php
vendored
37
vendor/psy/psysh/src/Command/EditCommand.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -14,6 +14,7 @@
|
||||
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;
|
||||
@@ -154,9 +155,41 @@ 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);
|
||||
\proc_close($proc);
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
2
vendor/psy/psysh/src/Command/ExitCommand.php
vendored
2
vendor/psy/psysh/src/Command/ExitCommand.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
141
vendor/psy/psysh/src/Command/HelpCommand.php
vendored
141
vendor/psy/psysh/src/Command/HelpCommand.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -11,7 +11,9 @@
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use Psy\Output\ShellOutput;
|
||||
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;
|
||||
@@ -24,7 +26,12 @@
|
||||
*/
|
||||
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}
|
||||
@@ -51,6 +58,14 @@ 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}
|
||||
*
|
||||
@@ -58,10 +73,13 @@ public function setCommand(Command $command)
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$shellOutput = $this->shellOutput($output);
|
||||
|
||||
if ($this->command !== null) {
|
||||
// help for an individual command
|
||||
$output->page($this->command->asText());
|
||||
$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 {
|
||||
@@ -78,42 +96,93 @@ protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->page($cmd->asText());
|
||||
if (!$cmd instanceof Command) {
|
||||
throw new \RuntimeException(\sprintf('Expected Psy\Command\Command instance, got %s', \get_class($cmd)));
|
||||
}
|
||||
|
||||
$shellOutput->page($cmd->asTextForInput($input));
|
||||
} else {
|
||||
// list available commands
|
||||
$commands = $this->getApplication()->all();
|
||||
|
||||
$table = $this->getTable($output);
|
||||
|
||||
foreach ($commands as $name => $command) {
|
||||
if ($name !== $command->getName()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($command->getAliases()) {
|
||||
$aliases = \sprintf('<comment>Aliases:</comment> %s', \implode(', ', $command->getAliases()));
|
||||
} else {
|
||||
$aliases = '';
|
||||
}
|
||||
|
||||
$table->addRow([
|
||||
\sprintf('<info>%s</info>', $name),
|
||||
$command->getDescription(),
|
||||
$aliases,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($output instanceof ShellOutput) {
|
||||
$output->startPaging();
|
||||
}
|
||||
|
||||
$table->render();
|
||||
|
||||
if ($output instanceof ShellOutput) {
|
||||
$output->stopPaging();
|
||||
}
|
||||
$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();
|
||||
}
|
||||
}
|
||||
|
||||
11
vendor/psy/psysh/src/Command/HistoryCommand.php
vendored
11
vendor/psy/psysh/src/Command/HistoryCommand.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -13,8 +13,9 @@
|
||||
|
||||
use Psy\ConfigPaths;
|
||||
use Psy\Input\FilterOptions;
|
||||
use Psy\Output\ShellOutput;
|
||||
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;
|
||||
@@ -25,7 +26,7 @@
|
||||
*
|
||||
* Shows, searches and replays readline history. Not too shabby.
|
||||
*/
|
||||
class HistoryCommand extends Command
|
||||
class HistoryCommand extends Command implements ReadlineAware
|
||||
{
|
||||
private FilterOptions $filter;
|
||||
private Readline $readline;
|
||||
@@ -151,12 +152,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
$this->clearHistory();
|
||||
$output->writeln('<info>History cleared.</info>');
|
||||
} else {
|
||||
$type = $input->getOption('no-numbers') ? 0 : ShellOutput::NUMBER_LINES;
|
||||
$type = $input->getOption('no-numbers') ? 0 : ShellOutputAdapter::NUMBER_LINES;
|
||||
if (!$highlighted) {
|
||||
$type = $type | OutputInterface::OUTPUT_RAW;
|
||||
}
|
||||
|
||||
$output->page($highlighted ?: $history, $type);
|
||||
$this->shellOutput($output)->page($highlighted ?: $history, $type);
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
23
vendor/psy/psysh/src/Command/ListCommand.php
vendored
23
vendor/psy/psysh/src/Command/ListCommand.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -22,7 +22,6 @@
|
||||
use Psy\Exception\RuntimeException;
|
||||
use Psy\Input\CodeArgument;
|
||||
use Psy\Input\FilterOptions;
|
||||
use Psy\Output\ShellOutput;
|
||||
use Psy\VarDumper\Presenter;
|
||||
use Psy\VarDumper\PresenterAware;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
@@ -118,6 +117,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->validateInput($input);
|
||||
$this->initEnumerators();
|
||||
$shellOutput = $this->shellOutput($output);
|
||||
|
||||
$method = $input->getOption('long') ? 'writeLong' : 'write';
|
||||
|
||||
@@ -127,17 +127,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
$reflector = null;
|
||||
}
|
||||
|
||||
// @todo something cleaner than this :-/
|
||||
if ($output instanceof ShellOutput && $input->getOption('long')) {
|
||||
$output->startPaging();
|
||||
if ($input->getOption('long')) {
|
||||
$shellOutput->startPaging();
|
||||
}
|
||||
|
||||
foreach ($this->enumerators as $enumerator) {
|
||||
$this->$method($output, $enumerator->enumerate($input, $reflector, $target));
|
||||
}
|
||||
|
||||
if ($output instanceof ShellOutput && $input->getOption('long')) {
|
||||
$output->stopPaging();
|
||||
if ($input->getOption('long')) {
|
||||
$shellOutput->stopPaging();
|
||||
}
|
||||
|
||||
// Set some magic local variables
|
||||
@@ -186,9 +185,7 @@ protected function write(OutputInterface $output, array $result)
|
||||
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(function ($item) use ($formatter) {
|
||||
return $formatter->format($this->formatItemName($item));
|
||||
}, $items);
|
||||
$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);
|
||||
@@ -213,9 +210,12 @@ protected function writeLong(OutputInterface $output, array $result)
|
||||
}
|
||||
|
||||
$table = $this->getTable($output);
|
||||
$first = true;
|
||||
|
||||
foreach ($result as $label => $items) {
|
||||
$output->writeln('');
|
||||
if (!$first) {
|
||||
$output->writeln('');
|
||||
}
|
||||
$output->writeln(\sprintf('<strong>%s:</strong>', $label));
|
||||
|
||||
$table->setRows([]);
|
||||
@@ -224,6 +224,7 @@ protected function writeLong(OutputInterface $output, array $result)
|
||||
}
|
||||
|
||||
$table->render();
|
||||
$first = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -95,9 +95,7 @@ protected function filterClasses(string $key, array $classes, bool $internal, bo
|
||||
}
|
||||
|
||||
if (!$user && !$internal) {
|
||||
$ret[$key] = \array_filter($classes, function ($class) use ($prefix) {
|
||||
return $prefix === null || \strpos(\strtolower($class), $prefix) === 0;
|
||||
});
|
||||
$ret[$key] = \array_filter($classes, fn ($class) => $prefix === null || \strpos(\strtolower($class), $prefix) === 0);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -15,6 +15,7 @@
|
||||
use Psy\Input\FilterOptions;
|
||||
use Psy\Util\Mirror;
|
||||
use Psy\VarDumper\Presenter;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
@@ -30,6 +31,7 @@ abstract class Enumerator
|
||||
const IS_CONSTANT = 'const';
|
||||
const IS_CLASS = 'class';
|
||||
const IS_FUNCTION = 'function';
|
||||
const IS_VIRTUAL = 'virtual';
|
||||
|
||||
private FilterOptions $filter;
|
||||
private Presenter $presenter;
|
||||
@@ -91,6 +93,12 @@ protected function showItem($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);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
namespace Psy\Command\ListCommand;
|
||||
|
||||
use Psy\Reflection\ReflectionMagicMethod;
|
||||
use Psy\Util\Docblock;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
@@ -59,7 +61,7 @@ protected function listItems(InputInterface $input, ?\Reflector $reflector = nul
|
||||
* @param \ReflectionClass $reflector
|
||||
* @param bool $noInherit Exclude inherited methods
|
||||
*
|
||||
* @return array
|
||||
* @return \ReflectionMethod[]
|
||||
*/
|
||||
protected function getMethods(bool $showAll, \ReflectionClass $reflector, bool $noInherit = false): array
|
||||
{
|
||||
@@ -78,6 +80,18 @@ protected function getMethods(bool $showAll, \ReflectionClass $reflector, bool $
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -86,7 +100,7 @@ protected function getMethods(bool $showAll, \ReflectionClass $reflector, bool $
|
||||
/**
|
||||
* Prepare formatted method array.
|
||||
*
|
||||
* @param array $methods
|
||||
* @param \ReflectionMethod[] $methods
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
@@ -127,10 +141,14 @@ protected function getKindLabel(\ReflectionClass $reflector): string
|
||||
/**
|
||||
* Get output style for the given method's visibility.
|
||||
*
|
||||
* @param \ReflectionMethod $method
|
||||
* @param \ReflectionMethod|ReflectionMagicMethod $method
|
||||
*/
|
||||
private function getVisibilityStyle(\ReflectionMethod $method): string
|
||||
private function getVisibilityStyle(\Reflector $method): string
|
||||
{
|
||||
if ($method instanceof ReflectionMagicMethod) {
|
||||
return self::IS_VIRTUAL;
|
||||
}
|
||||
|
||||
if ($method->isPublic()) {
|
||||
return self::IS_PUBLIC;
|
||||
} elseif ($method->isProtected()) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
namespace Psy\Command\ListCommand;
|
||||
|
||||
use Psy\Reflection\ReflectionMagicProperty;
|
||||
use Psy\Util\Docblock;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
@@ -60,7 +62,7 @@ protected function listItems(InputInterface $input, ?\Reflector $reflector = nul
|
||||
* @param \ReflectionClass $reflector
|
||||
* @param bool $noInherit Exclude inherited properties
|
||||
*
|
||||
* @return array
|
||||
* @return \ReflectionProperty[]
|
||||
*/
|
||||
protected function getProperties(bool $showAll, \ReflectionClass $reflector, bool $noInherit = false): array
|
||||
{
|
||||
@@ -77,6 +79,18 @@ protected function getProperties(bool $showAll, \ReflectionClass $reflector, boo
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -85,7 +99,7 @@ protected function getProperties(bool $showAll, \ReflectionClass $reflector, boo
|
||||
/**
|
||||
* Prepare formatted property array.
|
||||
*
|
||||
* @param array $properties
|
||||
* @param \ReflectionProperty[] $properties
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
@@ -125,10 +139,14 @@ protected function getKindLabel(\ReflectionClass $reflector): string
|
||||
/**
|
||||
* Get output style for the given property's visibility.
|
||||
*
|
||||
* @param \ReflectionProperty $property
|
||||
* @param \ReflectionProperty|ReflectionMagicProperty $property
|
||||
*/
|
||||
private function getVisibilityStyle(\ReflectionProperty $property): string
|
||||
private function getVisibilityStyle(\Reflector $property): string
|
||||
{
|
||||
if ($property instanceof ReflectionMagicProperty) {
|
||||
return self::IS_VIRTUAL;
|
||||
}
|
||||
|
||||
if ($property->isPublic()) {
|
||||
return self::IS_PUBLIC;
|
||||
} elseif ($property->isProtected()) {
|
||||
@@ -141,11 +159,16 @@ private function getVisibilityStyle(\ReflectionProperty $property): string
|
||||
/**
|
||||
* Present the $target's current value for a reflection property.
|
||||
*
|
||||
* @param \ReflectionProperty $property
|
||||
* @param mixed $target
|
||||
* @param \ReflectionProperty|ReflectionMagicProperty $property
|
||||
* @param mixed $target
|
||||
*/
|
||||
protected function presentValue(\ReflectionProperty $property, $target): string
|
||||
protected function presentValue(\Reflector $property, $target): string
|
||||
{
|
||||
// Magic properties use SignatureFormatter for display
|
||||
if ($property instanceof ReflectionMagicProperty) {
|
||||
return $this->presentSignature($property);
|
||||
}
|
||||
|
||||
if (!$target) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
namespace Psy\Command;
|
||||
|
||||
use PhpParser\Error as PhpParserError;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Parser;
|
||||
use Psy\Context;
|
||||
@@ -117,7 +118,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
|
||||
try {
|
||||
$nodes = $this->parser->parse($code);
|
||||
} catch (\PhpParser\Error $e) {
|
||||
} catch (PhpParserError $e) {
|
||||
if ($this->parseErrorIsEOF($e)) {
|
||||
$nodes = $this->parser->parse($code.';');
|
||||
} else {
|
||||
@@ -125,14 +126,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
}
|
||||
}
|
||||
|
||||
$output->page($this->presenter->present($nodes, $depth));
|
||||
$this->shellOutput($output)->page($this->presenter->present($nodes, $depth, Presenter::RAW), OutputInterface::OUTPUT_RAW);
|
||||
|
||||
$this->context->setReturnValue($nodes);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function parseErrorIsEOF(\PhpParser\Error $e): bool
|
||||
private function parseErrorIsEOF(PhpParserError $e): bool
|
||||
{
|
||||
$msg = $e->getRawMessage();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
15
vendor/psy/psysh/src/Command/ShowCommand.php
vendored
15
vendor/psy/psysh/src/Command/ShowCommand.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -102,6 +102,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
|
||||
private function writeCodeContext(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$shellOutput = $this->shellOutput($output);
|
||||
|
||||
try {
|
||||
list($target, $reflector) = $this->getTargetAndReflector($input->getArgument('target'), $output);
|
||||
} catch (UnexpectedTargetException $e) {
|
||||
@@ -116,7 +118,7 @@ private function writeCodeContext(InputInterface $input, OutputInterface $output
|
||||
]);
|
||||
}
|
||||
|
||||
$output->page(CodeFormatter::formatCode($code));
|
||||
$shellOutput->page(CodeFormatter::formatCode($code));
|
||||
|
||||
return;
|
||||
} else {
|
||||
@@ -128,7 +130,7 @@ private function writeCodeContext(InputInterface $input, OutputInterface $output
|
||||
$this->setCommandScopeVariables($reflector);
|
||||
|
||||
try {
|
||||
$output->page(CodeFormatter::format($reflector));
|
||||
$shellOutput->page(CodeFormatter::format($reflector));
|
||||
} catch (RuntimeException $e) {
|
||||
$output->writeln(SignatureFormatter::format($reflector));
|
||||
throw $e;
|
||||
@@ -167,9 +169,12 @@ private function writeExceptionContext(InputInterface $input, OutputInterface $o
|
||||
$this->lastException = $exception;
|
||||
$this->lastExceptionIndex = $index;
|
||||
|
||||
$output->writeln($this->getShell()->formatException($exception));
|
||||
$output->writeln('--');
|
||||
$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]);
|
||||
|
||||
5
vendor/psy/psysh/src/Command/SudoCommand.php
vendored
5
vendor/psy/psysh/src/Command/SudoCommand.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -15,6 +15,7 @@
|
||||
use PhpParser\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;
|
||||
@@ -22,7 +23,7 @@
|
||||
/**
|
||||
* Evaluate PHP code, bypassing visibility restrictions.
|
||||
*/
|
||||
class SudoCommand extends Command
|
||||
class SudoCommand extends Command implements ReadlineAware
|
||||
{
|
||||
private Readline $readline;
|
||||
private CodeArgumentParser $parser;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -13,11 +13,9 @@
|
||||
|
||||
use PhpParser\Node\Arg;
|
||||
use PhpParser\Node\Expr\New_;
|
||||
use PhpParser\Node\Expr\Throw_;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
|
||||
use PhpParser\Node\Scalar\String_;
|
||||
use PhpParser\Node\Stmt\Expression;
|
||||
use PhpParser\PrettyPrinter\Standard as Printer;
|
||||
use Psy\Exception\ThrowUpException;
|
||||
use Psy\Input\CodeArgument;
|
||||
@@ -79,8 +77,8 @@ protected function configure(): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$args = $this->prepareArgs($input->getArgument('exception'));
|
||||
$throwStmt = new Expression(new Throw_(new New_(new FullyQualifiedName(ThrowUpException::class), $args)));
|
||||
$throwCode = $this->printer->prettyPrint([$throwStmt]);
|
||||
$exception = new New_(new FullyQualifiedName(ThrowUpException::class), $args);
|
||||
$throwCode = 'throw '.$this->printer->prettyPrintExpr($exception).';';
|
||||
|
||||
$shell = $this->getShell();
|
||||
$shell->addCode($throwCode, !$shell->hasCode());
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -27,8 +27,9 @@ 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!
|
||||
private static ?int $start = null;
|
||||
// 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;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
use Psy\Formatter\TraceFormatter;
|
||||
use Psy\Input\FilterOptions;
|
||||
use Psy\Output\ShellOutput;
|
||||
use Psy\Output\ShellOutputAdapter;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
@@ -75,7 +75,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->filter->bind($input);
|
||||
$trace = $this->getBacktrace(new \Exception(), $input->getOption('num'), $input->getOption('include-psy'));
|
||||
$output->page($trace, ShellOutput::NUMBER_LINES);
|
||||
$this->shellOutput($output)->page($trace, ShellOutputAdapter::NUMBER_LINES);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
13
vendor/psy/psysh/src/Command/WhereamiCommand.php
vendored
13
vendor/psy/psysh/src/Command/WhereamiCommand.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
use Psy\ConfigPaths;
|
||||
use Psy\Formatter\CodeFormatter;
|
||||
use Psy\Output\ShellOutput;
|
||||
use Psy\Shell;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -112,6 +111,8 @@ protected function fileInfo(): array
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$shellOutput = $this->shellOutput($output);
|
||||
|
||||
$info = $this->fileInfo();
|
||||
$num = $input->getOption('num');
|
||||
$lineNum = $info['line'];
|
||||
@@ -124,16 +125,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
$endLine = null;
|
||||
}
|
||||
|
||||
if ($output instanceof ShellOutput) {
|
||||
$output->startPaging();
|
||||
}
|
||||
$shellOutput->startPaging();
|
||||
|
||||
$output->writeln(\sprintf('From <info>%s:%s</info>:', ConfigPaths::prettyPath($info['file']), $lineNum));
|
||||
$output->write(CodeFormatter::formatCode($code, $startLine, $endLine, $lineNum), false);
|
||||
|
||||
if ($output instanceof ShellOutput) {
|
||||
$output->stopPaging();
|
||||
}
|
||||
$shellOutput->stopPaging();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
31
vendor/psy/psysh/src/Command/WtfCommand.php
vendored
31
vendor/psy/psysh/src/Command/WtfCommand.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -14,7 +14,7 @@
|
||||
use Psy\Context;
|
||||
use Psy\ContextAware;
|
||||
use Psy\Input\FilterOptions;
|
||||
use Psy\Output\ShellOutput;
|
||||
use Psy\Output\ShellOutputAdapter;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -82,6 +82,7 @@ protected function configure(): void
|
||||
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))) {
|
||||
@@ -90,11 +91,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
|
||||
$exception = $this->context->getLastException();
|
||||
$count = $input->getOption('all') ? \PHP_INT_MAX : \max(3, \pow(2, \strlen($incredulity) + 1));
|
||||
$shell = $this->getShell();
|
||||
|
||||
if ($output instanceof ShellOutput) {
|
||||
$output->startPaging();
|
||||
}
|
||||
|
||||
$shellOutput->startPaging();
|
||||
do {
|
||||
$traceCount = \count($exception->getTrace());
|
||||
$showLines = $count;
|
||||
@@ -106,23 +105,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
$trace = $this->getBacktrace($exception, $showLines);
|
||||
$moreLines = $traceCount - \count($trace);
|
||||
|
||||
$output->writeln($this->getShell()->formatException($exception));
|
||||
$output->writeln('--');
|
||||
$output->write($trace, true, ShellOutput::NUMBER_LINES);
|
||||
$output->writeln('');
|
||||
$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
|
||||
));
|
||||
$output->writeln('');
|
||||
}
|
||||
} while ($exception = $exception->getPrevious());
|
||||
|
||||
if ($output instanceof ShellOutput) {
|
||||
$output->stopPaging();
|
||||
}
|
||||
$previous = $exception->getPrevious();
|
||||
if ($previous !== null) {
|
||||
$shell->writeSpacer($output);
|
||||
}
|
||||
} while ($exception = $previous);
|
||||
|
||||
$shellOutput->stopPaging();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
5
vendor/psy/psysh/src/Command/YoloCommand.php
vendored
5
vendor/psy/psysh/src/Command/YoloCommand.php
vendored
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
* (c) 2012-2026 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
@@ -13,13 +13,14 @@
|
||||
|
||||
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
|
||||
class YoloCommand extends Command implements ReadlineAware
|
||||
{
|
||||
private Readline $readline;
|
||||
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user