update lock clucknut
All checks were successful
All checks were successful
This commit is contained in:
543
vendor/psy/psysh/src/Formatter/CodeFormatter.php
vendored
543
vendor/psy/psysh/src/Formatter/CodeFormatter.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,6 +13,7 @@
|
||||
|
||||
use Psy\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
|
||||
|
||||
/**
|
||||
* A pretty-printer for code.
|
||||
@@ -28,14 +29,19 @@ class CodeFormatter implements ReflectorFormatter
|
||||
const HIGHLIGHT_PUBLIC = 'public';
|
||||
const HIGHLIGHT_PROTECTED = 'protected';
|
||||
const HIGHLIGHT_PRIVATE = 'private';
|
||||
const HIGHLIGHT_CLASS = 'class';
|
||||
|
||||
const HIGHLIGHT_BOOL = 'bool';
|
||||
const HIGHLIGHT_CONST = 'const';
|
||||
const HIGHLIGHT_NUMBER = 'number';
|
||||
const HIGHLIGHT_STRING = 'string';
|
||||
const HIGHLIGHT_ARRAY_KEY = 'array_key';
|
||||
const HIGHLIGHT_COMMENT = 'code_comment';
|
||||
const HIGHLIGHT_INLINE_HTML = 'inline_html';
|
||||
|
||||
private const TOKEN_MAP = [
|
||||
private const CLASS_REFERENCE_KEYWORDS = [\T_NEW, \T_INSTANCEOF, \T_CATCH];
|
||||
|
||||
private const BASE_TOKEN_MAP = [
|
||||
// Not highlighted
|
||||
\T_OPEN_TAG => self::HIGHLIGHT_DEFAULT,
|
||||
\T_OPEN_TAG_WITH_ECHO => self::HIGHLIGHT_DEFAULT,
|
||||
@@ -73,6 +79,71 @@ class CodeFormatter implements ReflectorFormatter
|
||||
\T_INLINE_HTML => self::HIGHLIGHT_INLINE_HTML,
|
||||
];
|
||||
|
||||
private const KEYWORD_TOKENS = [
|
||||
\T_ABSTRACT,
|
||||
\T_ARRAY,
|
||||
\T_AS,
|
||||
\T_BREAK,
|
||||
\T_CALLABLE,
|
||||
\T_CASE,
|
||||
\T_CATCH,
|
||||
\T_CLONE,
|
||||
\T_CONTINUE,
|
||||
\T_DECLARE,
|
||||
\T_DEFAULT,
|
||||
\T_DO,
|
||||
\T_ECHO,
|
||||
\T_ELSE,
|
||||
\T_ELSEIF,
|
||||
\T_EMPTY,
|
||||
\T_ENDDECLARE,
|
||||
\T_ENDFOR,
|
||||
\T_ENDFOREACH,
|
||||
\T_ENDIF,
|
||||
\T_ENDSWITCH,
|
||||
\T_ENDWHILE,
|
||||
\T_EVAL,
|
||||
\T_EXIT,
|
||||
\T_EXTENDS,
|
||||
\T_FINAL,
|
||||
\T_FINALLY,
|
||||
\T_FN,
|
||||
\T_FOR,
|
||||
\T_FOREACH,
|
||||
\T_FUNCTION,
|
||||
\T_GLOBAL,
|
||||
\T_GOTO,
|
||||
\T_IF,
|
||||
\T_IMPLEMENTS,
|
||||
\T_INCLUDE,
|
||||
\T_INCLUDE_ONCE,
|
||||
\T_INSTANCEOF,
|
||||
\T_INSTEADOF,
|
||||
\T_INTERFACE,
|
||||
\T_ISSET,
|
||||
\T_LIST,
|
||||
\T_LOGICAL_AND,
|
||||
\T_LOGICAL_OR,
|
||||
\T_LOGICAL_XOR,
|
||||
\T_NAMESPACE,
|
||||
\T_NEW,
|
||||
\T_PRINT,
|
||||
\T_REQUIRE,
|
||||
\T_REQUIRE_ONCE,
|
||||
\T_RETURN,
|
||||
\T_STATIC,
|
||||
\T_SWITCH,
|
||||
\T_THROW,
|
||||
\T_TRAIT,
|
||||
\T_TRY,
|
||||
\T_UNSET,
|
||||
\T_USE,
|
||||
\T_VAR,
|
||||
\T_WHILE,
|
||||
\T_YIELD,
|
||||
\T_YIELD_FROM,
|
||||
];
|
||||
|
||||
/**
|
||||
* Format the code represented by $reflector for shell output.
|
||||
*
|
||||
@@ -115,6 +186,52 @@ public static function formatCode(string $code, int $startLine = 1, ?int $endLin
|
||||
return \implode('', \iterator_to_array($lines));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a PHP snippet into ANSI-safe lines for inline shell rendering.
|
||||
*
|
||||
* Styles are applied line-by-line so multiline tokens do not leak styling
|
||||
* across prompt boundaries.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function formatInputLines(string $code, ?OutputFormatterInterface $formatter = null): array
|
||||
{
|
||||
$lines = [''];
|
||||
$lineIndex = 0;
|
||||
$first = true;
|
||||
|
||||
foreach (self::tokenizeSpans('<?php '.$code) as [$spanType, $spanText]) {
|
||||
if ($first) {
|
||||
$spanText = (string) \substr($spanText, \strlen('<?php '));
|
||||
$first = false;
|
||||
|
||||
if ($spanText === '') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$parts = \preg_split('/(\r\n?|\n)/', $spanText, -1, \PREG_SPLIT_DELIM_CAPTURE);
|
||||
if ($parts === false) {
|
||||
$parts = [$spanText];
|
||||
}
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if ($part === "\r" || $part === "\n" || $part === "\r\n") {
|
||||
$lines[++$lineIndex] = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($part === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lines[$lineIndex] .= self::formatInputSpan($spanType, $part, $formatter);
|
||||
}
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start line for a given Reflector.
|
||||
*
|
||||
@@ -149,11 +266,22 @@ private static function getStartLine(\Reflector $reflector): int
|
||||
*/
|
||||
private static function tokenizeSpans(string $code): \Generator
|
||||
{
|
||||
$tokens = \token_get_all($code);
|
||||
$arrayKeyIndexes = self::findArrayKeyTokenIndexes($tokens);
|
||||
[$classNameIndexes, $keywordNameIndexes] = self::findClassNameTokenIndexes($tokens);
|
||||
$spanType = null;
|
||||
$buffer = '';
|
||||
|
||||
foreach (\token_get_all($code) as $token) {
|
||||
$nextType = self::nextHighlightType($token, $spanType);
|
||||
foreach ($tokens as $index => $token) {
|
||||
if (isset($arrayKeyIndexes[$index])) {
|
||||
$nextType = self::HIGHLIGHT_ARRAY_KEY;
|
||||
} elseif (isset($keywordNameIndexes[$index])) {
|
||||
$nextType = self::HIGHLIGHT_KEYWORD;
|
||||
} elseif (isset($classNameIndexes[$index])) {
|
||||
$nextType = self::HIGHLIGHT_CLASS;
|
||||
} else {
|
||||
$nextType = self::nextHighlightType($token);
|
||||
}
|
||||
$spanType = $spanType ?: $nextType;
|
||||
|
||||
if ($spanType !== $nextType) {
|
||||
@@ -171,30 +299,408 @@ private static function tokenizeSpans(string $code): \Generator
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a token and the current highlight span type, compute the next type.
|
||||
* Given a token, compute the highlight type from the token map.
|
||||
*
|
||||
* @param array|string $token \token_get_all token
|
||||
* @param string|null $currentType
|
||||
* @param array|string $token \token_get_all token
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private static function nextHighlightType($token, $currentType)
|
||||
private static function nextHighlightType($token)
|
||||
{
|
||||
if ($token === '"') {
|
||||
return self::HIGHLIGHT_STRING;
|
||||
}
|
||||
|
||||
if (\is_array($token)) {
|
||||
if ($token[0] === \T_WHITESPACE) {
|
||||
return $currentType;
|
||||
if (($literalHighlight = self::literalHighlightType($token)) !== null) {
|
||||
return $literalHighlight;
|
||||
}
|
||||
|
||||
if (\array_key_exists($token[0], self::TOKEN_MAP)) {
|
||||
return self::TOKEN_MAP[$token[0]];
|
||||
$tokenMap = self::getTokenMap();
|
||||
if (\array_key_exists($token[0], $tokenMap)) {
|
||||
return $tokenMap[$token[0]];
|
||||
}
|
||||
}
|
||||
|
||||
return self::HIGHLIGHT_KEYWORD;
|
||||
return self::HIGHLIGHT_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the token-to-style map once so unsupported token constants on older
|
||||
* PHP versions can be ignored safely.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private static function getTokenMap(): array
|
||||
{
|
||||
static $tokenMap = null;
|
||||
|
||||
if ($tokenMap !== null) {
|
||||
return $tokenMap;
|
||||
}
|
||||
|
||||
$tokenMap = self::BASE_TOKEN_MAP;
|
||||
|
||||
foreach (self::KEYWORD_TOKENS as $tokenType) {
|
||||
$tokenMap[$tokenType] = self::HIGHLIGHT_KEYWORD;
|
||||
}
|
||||
|
||||
if (\defined('T_MATCH')) {
|
||||
$tokenMap[\constant('T_MATCH')] = self::HIGHLIGHT_KEYWORD;
|
||||
}
|
||||
|
||||
return $tokenMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark string literal tokens that are serving as array keys.
|
||||
*
|
||||
* This stays token-based instead of requiring a valid AST so it can work
|
||||
* during interactive editing on incomplete input.
|
||||
*
|
||||
* @param array<int, array|string> $tokens
|
||||
*
|
||||
* @return array<int, true>
|
||||
*/
|
||||
private static function findArrayKeyTokenIndexes(array $tokens): array
|
||||
{
|
||||
$arrayKeyIndexes = [];
|
||||
$stack = [];
|
||||
$pendingArrayParen = false;
|
||||
|
||||
foreach ($tokens as $index => $token) {
|
||||
if (\is_array($token)) {
|
||||
if ($token[0] === \T_ARRAY) {
|
||||
$pendingArrayParen = true;
|
||||
}
|
||||
|
||||
if ($token[0] === \T_CONSTANT_ENCAPSED_STRING && self::isArrayContext($stack) && self::nextSignificantTokenIsDoubleArrow($tokens, $index)) {
|
||||
$arrayKeyIndexes[$index] = true;
|
||||
}
|
||||
|
||||
if (!self::isIgnorableToken($token) && $token[0] !== \T_ARRAY) {
|
||||
$pendingArrayParen = false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($token === '(') {
|
||||
$stack[] = [
|
||||
'closing' => ')',
|
||||
'arrayLike' => $pendingArrayParen,
|
||||
];
|
||||
$pendingArrayParen = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($token === '[') {
|
||||
$stack[] = [
|
||||
'closing' => ']',
|
||||
'arrayLike' => true,
|
||||
];
|
||||
$pendingArrayParen = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($token === ')' || $token === ']') && !empty($stack)) {
|
||||
$top = \array_pop($stack);
|
||||
if (($top['closing'] ?? null) !== $token) {
|
||||
$stack = [];
|
||||
}
|
||||
}
|
||||
|
||||
if ($token !== ',' && $token !== '=>' && $token !== '=') {
|
||||
$pendingArrayParen = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $arrayKeyIndexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{closing: string, arrayLike: bool}> $stack
|
||||
*/
|
||||
private static function isArrayContext(array $stack): bool
|
||||
{
|
||||
if ($stack === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$top = $stack[\count($stack) - 1];
|
||||
|
||||
return !empty($top['arrayLike']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array|string> $tokens
|
||||
*/
|
||||
private static function nextSignificantTokenIsDoubleArrow(array $tokens, int $index): bool
|
||||
{
|
||||
$count = \count($tokens);
|
||||
|
||||
for ($i = $index + 1; $i < $count; $i++) {
|
||||
$token = $tokens[$i];
|
||||
|
||||
if (self::isIgnorableToken($token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return \is_array($token) && $token[0] === \T_DOUBLE_ARROW;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark tokens that are very likely to be class names from immediate syntax context.
|
||||
*
|
||||
* This is intentionally heuristic and conservative: declaration names,
|
||||
* extends/implements lists, obvious runtime class references (`new`,
|
||||
* `instanceof`, `catch`), and static references like `Foo::class`.
|
||||
*
|
||||
* On PHP 8+, qualified names arrive as T_NAME_* tokens and can be marked as
|
||||
* a single unit. On PHP 7.4, qualified names are split across T_STRING and
|
||||
* T_NS_SEPARATOR tokens, so we stitch them together only in these narrow,
|
||||
* class-specific contexts.
|
||||
*
|
||||
* @param array<int, array|string> $tokens
|
||||
*
|
||||
* @return array{0: array<int, true>, 1: array<int, true>}
|
||||
*/
|
||||
private static function findClassNameTokenIndexes(array $tokens): array
|
||||
{
|
||||
$classIndexes = [];
|
||||
$keywordIndexes = [];
|
||||
$context = null;
|
||||
|
||||
foreach ($tokens as $index => $token) {
|
||||
if (self::isIgnorableToken($token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($context !== null) {
|
||||
if (self::literalHighlightType($token) !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (self::isClassNameToken($token)) {
|
||||
if (self::isPseudoClassToken($token)) {
|
||||
$keywordIndexes[$index] = true;
|
||||
} else {
|
||||
$classIndexes[$index] = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (self::isNamespaceSeparatorToken($token)) {
|
||||
$classIndexes[$index] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($context === 'list' && ($token === ',' || $token === '&')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($context === 'union' && ($token === '|' || $token === '&')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($context === 'union' && $token === '?') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context = null;
|
||||
}
|
||||
|
||||
if (!\is_array($token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tokenType = $token[0];
|
||||
|
||||
if (self::isClassDeclarationKeyword($tokenType) && !self::previousSignificantTokenIs($tokens, $index, \T_DOUBLE_COLON)) {
|
||||
$context = 'single';
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($tokenType === \T_EXTENDS || $tokenType === \T_IMPLEMENTS) {
|
||||
$context = 'list';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\in_array($tokenType, self::CLASS_REFERENCE_KEYWORDS, true)) {
|
||||
$context = 'union';
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($tokenType === \T_DOUBLE_COLON) {
|
||||
self::markPreviousNameToken($tokens, $classIndexes, $keywordIndexes, $index - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return [$classIndexes, $keywordIndexes];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $token
|
||||
*/
|
||||
private static function isIgnorableToken($token): bool
|
||||
{
|
||||
return \is_array($token) && ($token[0] === \T_WHITESPACE || $token[0] === \T_COMMENT || $token[0] === \T_DOC_COMMENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $token
|
||||
*/
|
||||
private static function isClassNameToken($token): bool
|
||||
{
|
||||
if (!\is_array($token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($token[0] === \T_STRING) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return \in_array($token[0], self::qualifiedNameTokenTypes(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $token
|
||||
*/
|
||||
private static function isNamespaceSeparatorToken($token): bool
|
||||
{
|
||||
return \is_array($token) && $token[0] === \T_NS_SEPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
private static function qualifiedNameTokenTypes(): array
|
||||
{
|
||||
static $types = null;
|
||||
|
||||
if ($types !== null) {
|
||||
return $types;
|
||||
}
|
||||
|
||||
$types = [];
|
||||
|
||||
foreach (['T_NAME_QUALIFIED', 'T_NAME_FULLY_QUALIFIED', 'T_NAME_RELATIVE'] as $tokenName) {
|
||||
if (\defined($tokenName)) {
|
||||
$types[] = \constant($tokenName);
|
||||
}
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
private static function isClassDeclarationKeyword(int $tokenType): bool
|
||||
{
|
||||
if (\in_array($tokenType, [\T_CLASS, \T_INTERFACE, \T_TRAIT], true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return \defined('T_ENUM') && $tokenType === \constant('T_ENUM');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array|string> $tokens
|
||||
*/
|
||||
private static function previousSignificantTokenIs(array $tokens, int $index, int $tokenType): bool
|
||||
{
|
||||
for ($i = $index - 1; $i >= 0; $i--) {
|
||||
$token = $tokens[$i];
|
||||
|
||||
if (self::isIgnorableToken($token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return \is_array($token) && $token[0] === $tokenType;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array|string> $tokens
|
||||
* @param array<int, true> $classIndexes
|
||||
* @param array<int, true> $keywordIndexes
|
||||
*/
|
||||
private static function markPreviousNameToken(array $tokens, array &$classIndexes, array &$keywordIndexes, int $index): void
|
||||
{
|
||||
$chainIndexes = [];
|
||||
|
||||
for ($i = $index; $i >= 0; $i--) {
|
||||
$token = $tokens[$i];
|
||||
|
||||
if (self::isIgnorableToken($token)) {
|
||||
if ($chainIndexes === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (self::isNamespaceSeparatorToken($token) || self::isClassNameToken($token)) {
|
||||
$chainIndexes[] = $i;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($chainIndexes === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$firstIndex = $chainIndexes[0];
|
||||
if (self::literalHighlightType($tokens[$firstIndex]) !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::isPseudoClassToken($tokens[$firstIndex])) {
|
||||
$keywordIndexes[$firstIndex] = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($chainIndexes as $chainIndex) {
|
||||
$classIndexes[$chainIndex] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $token
|
||||
*/
|
||||
private static function isPseudoClassToken($token): bool
|
||||
{
|
||||
return \is_array($token)
|
||||
&& $token[0] === \T_STRING
|
||||
&& \in_array(\strtolower($token[1]), ['self', 'parent', 'static'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $token
|
||||
*/
|
||||
private static function literalHighlightType($token): ?string
|
||||
{
|
||||
if (!\is_array($token) || $token[0] !== \T_STRING) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = \strtolower($token[1]);
|
||||
|
||||
if ($value === 'true' || $value === 'false') {
|
||||
return self::HIGHLIGHT_BOOL;
|
||||
}
|
||||
|
||||
if ($value === 'null') {
|
||||
return self::HIGHLIGHT_CONST;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,7 +807,7 @@ private static function numberLines(\Generator $lines, ?int $markLine = null): \
|
||||
$mark = ($markLine === $lineNum) ? self::LINE_MARKER : self::NO_LINE_MARKER;
|
||||
}
|
||||
|
||||
yield \sprintf("%s<aside>%{$pad}s</aside>: %s", $mark, $lineNum, $line);
|
||||
yield \sprintf("%s<whisper>%{$pad}s:</whisper> %s", $mark, $lineNum, $line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,4 +822,13 @@ private static function isReflectable(\Reflector $reflector): bool
|
||||
{
|
||||
return ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionFunctionAbstract) && \is_file($reflector->getFileName());
|
||||
}
|
||||
|
||||
private static function formatInputSpan(string $spanType, string $spanText, ?OutputFormatterInterface $formatter): string
|
||||
{
|
||||
if ($spanType === self::HIGHLIGHT_DEFAULT || $formatter === null || !$formatter->isDecorated() || !$formatter->hasStyle($spanType)) {
|
||||
return $spanText;
|
||||
}
|
||||
|
||||
return $formatter->getStyle($spanType)->apply($spanText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -87,13 +87,7 @@ private static function formatVector(array $vector, array $lines): string
|
||||
$template = \implode(' ', $template);
|
||||
|
||||
return \implode("\n", \array_map(function ($line) use ($template) {
|
||||
$escaped = \array_map(function ($l) {
|
||||
if ($l === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return OutputFormatter::escape($l);
|
||||
}, $line);
|
||||
$escaped = \array_map(fn ($l) => ($l === null) ? '' : OutputFormatter::escape($l), $line);
|
||||
|
||||
return \rtrim(\vsprintf($template, $escaped));
|
||||
}, $lines));
|
||||
|
||||
@@ -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,8 @@
|
||||
use Psy\Manual\ManualInterface;
|
||||
use Psy\Reflection\ReflectionConstant;
|
||||
use Psy\Reflection\ReflectionLanguageConstruct;
|
||||
use Psy\Reflection\ReflectionMagicMethod;
|
||||
use Psy\Reflection\ReflectionMagicProperty;
|
||||
use Psy\Util\Json;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
|
||||
@@ -81,9 +83,15 @@ public static function format(\Reflector $reflector): string
|
||||
case $reflector instanceof \ReflectionClassConstant:
|
||||
return self::formatClassConstant($reflector);
|
||||
|
||||
case $reflector instanceof ReflectionMagicMethod:
|
||||
return self::formatMagicMethod($reflector);
|
||||
|
||||
case $reflector instanceof \ReflectionMethod:
|
||||
return self::formatMethod($reflector);
|
||||
|
||||
case $reflector instanceof ReflectionMagicProperty:
|
||||
return self::formatMagicProperty($reflector);
|
||||
|
||||
case $reflector instanceof \ReflectionProperty:
|
||||
return self::formatProperty($reflector);
|
||||
|
||||
@@ -104,7 +112,7 @@ public static function format(\Reflector $reflector): string
|
||||
*/
|
||||
public static function formatName(\Reflector $reflector): string
|
||||
{
|
||||
return $reflector->getName();
|
||||
return self::normalizeName($reflector->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,9 +124,12 @@ public static function formatName(\Reflector $reflector): string
|
||||
*/
|
||||
private static function formatModifiers(\Reflector $reflector): string
|
||||
{
|
||||
return \implode(' ', \array_map(function ($modifier) {
|
||||
return \sprintf('<keyword>%s</keyword>', $modifier);
|
||||
}, \Reflection::getModifierNames($reflector->getModifiers())));
|
||||
$modifiers = \array_map(
|
||||
fn ($modifier) => \sprintf('<keyword>%s</keyword>', $modifier),
|
||||
\Reflection::getModifierNames($reflector->getModifiers())
|
||||
);
|
||||
|
||||
return \implode(' ', $modifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,7 +158,7 @@ private static function formatClass(\ReflectionClass $reflector): string
|
||||
if ($parent = $reflector->getParentClass()) {
|
||||
$chunks[] = 'extends';
|
||||
$parentHref = self::getManualHref($parent);
|
||||
$chunks[] = LinkFormatter::styleWithHref('class', $parent->getName(), $parentHref);
|
||||
$chunks[] = LinkFormatter::styleWithHref('class', self::normalizeName($parent->getName()), $parentHref);
|
||||
}
|
||||
|
||||
$interfaces = $reflector->getInterfaceNames();
|
||||
@@ -162,7 +173,7 @@ private static function formatClass(\ReflectionClass $reflector): string
|
||||
$interfaceHref = null;
|
||||
}
|
||||
|
||||
return LinkFormatter::styleWithHref('class', $name, $interfaceHref);
|
||||
return LinkFormatter::styleWithHref('class', self::normalizeName($name), $interfaceHref);
|
||||
}, $interfaces));
|
||||
}
|
||||
|
||||
@@ -295,6 +306,68 @@ private static function formatMethod(\ReflectionMethod $reflector): string
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a magic method signature.
|
||||
*
|
||||
* @param ReflectionMagicMethod $reflector
|
||||
*
|
||||
* @return string Formatted signature
|
||||
*/
|
||||
private static function formatMagicMethod(ReflectionMagicMethod $reflector): string
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
$parts[] = self::formatModifiers($reflector);
|
||||
$parts[] = '<keyword>function</keyword>';
|
||||
|
||||
$ref = $reflector->returnsReference() ? '&' : '';
|
||||
|
||||
$signature = \sprintf(
|
||||
'%s<function>%s</function>(%s)',
|
||||
$ref,
|
||||
$reflector->getName(),
|
||||
OutputFormatter::escape($reflector->getParameterString())
|
||||
);
|
||||
|
||||
$returnType = $reflector->getDocblockReturnType();
|
||||
if ($returnType !== null) {
|
||||
$signature .= \sprintf(': <keyword>%s</keyword>', OutputFormatter::escape($returnType));
|
||||
}
|
||||
|
||||
$parts[] = $signature;
|
||||
|
||||
return \implode(' ', \array_filter($parts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a magic property signature.
|
||||
*
|
||||
* @param ReflectionMagicProperty $reflector
|
||||
*
|
||||
* @return string Formatted signature
|
||||
*/
|
||||
private static function formatMagicProperty(ReflectionMagicProperty $reflector): string
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
$parts[] = self::formatModifiers($reflector);
|
||||
|
||||
$type = $reflector->getDocblockType();
|
||||
if ($type !== null) {
|
||||
$parts[] = \sprintf('<keyword>%s</keyword>', OutputFormatter::escape($type));
|
||||
}
|
||||
|
||||
$parts[] = \sprintf('<strong>$%s</strong>', $reflector->getName());
|
||||
|
||||
if ($reflector->isReadOnly()) {
|
||||
$parts[] = '<aside>(read-only)</aside>';
|
||||
} elseif ($reflector->isWriteOnly()) {
|
||||
$parts[] = '<aside>(write-only)</aside>';
|
||||
}
|
||||
|
||||
return \implode(' ', \array_filter($parts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the function params.
|
||||
*
|
||||
@@ -316,7 +389,7 @@ private static function formatFunctionParams(\ReflectionFunctionAbstract $reflec
|
||||
if ($param->isArray()) {
|
||||
$hint = '<keyword>array</keyword>';
|
||||
} elseif ($class = $param->getClass()) {
|
||||
$hint = LinkFormatter::styleWithHref('class', $class->getName(), self::getManualHref($class));
|
||||
$hint = LinkFormatter::styleWithHref('class', self::normalizeName($class->getName()), self::getManualHref($class));
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
@@ -395,7 +468,7 @@ private static function formatReflectionType(?\ReflectionType $type, bool $indic
|
||||
private static function formatReflectionNamedType(\ReflectionNamedType $type, bool $indicateNullable): string
|
||||
{
|
||||
$nullable = $indicateNullable && $type->allowsNull() ? '?' : '';
|
||||
$typeName = $type->getName();
|
||||
$typeName = self::normalizeName($type->getName());
|
||||
|
||||
if ($type->isBuiltin()) {
|
||||
return \sprintf('<keyword>%s%s</keyword>', $nullable, OutputFormatter::escape($typeName));
|
||||
@@ -447,17 +520,17 @@ private static function getManualHref(\Reflector $reflector): ?string
|
||||
case \ReflectionClass::class:
|
||||
case \ReflectionObject::class:
|
||||
case \ReflectionFunction::class:
|
||||
$query = $reflector->name;
|
||||
$query = self::normalizeName($reflector->name);
|
||||
break;
|
||||
|
||||
case \ReflectionMethod::class:
|
||||
$query = $reflector->class.'.'.$reflector->name;
|
||||
$query = self::normalizeName($reflector->class).'.'.self::normalizeName($reflector->name);
|
||||
break;
|
||||
|
||||
case \ReflectionProperty::class:
|
||||
case \ReflectionClassConstant::class:
|
||||
// No simple redirect URLs for properties/constants, link to class instead
|
||||
$query = $reflector->class;
|
||||
$query = self::normalizeName($reflector->class);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -484,23 +557,20 @@ private static function getManualDoc(\Reflector $reflector)
|
||||
case \ReflectionClass::class:
|
||||
case \ReflectionObject::class:
|
||||
case \ReflectionFunction::class:
|
||||
$id = $reflector->name;
|
||||
$id = self::normalizeName($reflector->name);
|
||||
break;
|
||||
|
||||
case \ReflectionMethod::class:
|
||||
$id = $reflector->class.'::'.$reflector->name;
|
||||
case \ReflectionClassConstant::class:
|
||||
$id = self::normalizeName($reflector->class).'::'.self::normalizeName($reflector->name);
|
||||
break;
|
||||
|
||||
case \ReflectionProperty::class:
|
||||
$id = $reflector->class.'::$'.$reflector->name;
|
||||
break;
|
||||
|
||||
case \ReflectionClassConstant::class:
|
||||
$id = $reflector->class.'::'.$reflector->name;
|
||||
$id = self::normalizeName($reflector->class).'::$'.self::normalizeName($reflector->name);
|
||||
break;
|
||||
|
||||
case ReflectionConstant::class:
|
||||
$id = $reflector->name;
|
||||
$id = self::normalizeName($reflector->name);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -509,4 +579,12 @@ private static function getManualDoc(\Reflector $reflector)
|
||||
|
||||
return self::$manual->get($id) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip scoped PHAR namespace prefixes from a name.
|
||||
*/
|
||||
private static function normalizeName(string $name): string
|
||||
{
|
||||
return (string) \preg_replace('/^_Psy[a-f0-9]+\\\\/i', '', $name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user