diff --git a/admin/framework/composer.json b/admin/framework/composer.json index b507710c04b0..d03e43e18915 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -20,7 +20,7 @@ "codeigniter/coding-standard": "^1.7", "fakerphp/faker": "^1.9", "friendsofphp/php-cs-fixer": "^3.47.1", - "kint-php/kint": "^5.0.4", + "kint-php/kint": "^6.0", "mikey179/vfsstream": "^1.6", "nexusphp/cs-config": "^3.6", "phpunit/phpunit": "^10.5.16 || ^11.2", diff --git a/app/Config/Kint.php b/app/Config/Kint.php index d07078270145..931ad47f5fe4 100644 --- a/app/Config/Kint.php +++ b/app/Config/Kint.php @@ -3,7 +3,6 @@ namespace Config; use Kint\Parser\ConstructablePluginInterface; -use Kint\Renderer\AbstractRenderer; use Kint\Renderer\Rich\TabPluginInterface; use Kint\Renderer\Rich\ValuePluginInterface; @@ -41,7 +40,6 @@ class Kint */ public string $richTheme = 'aante-light.css'; public bool $richFolder = false; - public int $richSort = AbstractRenderer::SORT_FULL; /** * @var array>|null diff --git a/composer.json b/composer.json index 1704c70baa06..a46df741d51a 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "require-dev": { "codeigniter/phpstan-codeigniter": "^1.4", "fakerphp/faker": "^1.9", - "kint-php/kint": "^5.0.4", + "kint-php/kint": "^6.0", "mikey179/vfsstream": "^1.6", "nexusphp/tachycardia": "^2.0", "phpstan/extension-installer": "^1.4", diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 3c2a250f6600..0c5c7fab50fa 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -547,7 +547,7 @@ private function configureKint(): void RichRenderer::$theme = $config->richTheme; RichRenderer::$folder = $config->richFolder; - RichRenderer::$sort = $config->richSort; + if (isset($config->richObjectPlugins) && is_array($config->richObjectPlugins)) { RichRenderer::$value_plugins = $config->richObjectPlugins; } diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index bb68c33f0760..8ff5d2ae5b9b 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -294,7 +294,7 @@ private function configureKint(): void RichRenderer::$theme = $config->richTheme; RichRenderer::$folder = $config->richFolder; - RichRenderer::$sort = $config->richSort; + if (isset($config->richObjectPlugins) && is_array($config->richObjectPlugins)) { RichRenderer::$value_plugins = $config->richObjectPlugins; } diff --git a/system/ThirdParty/Kint/CallFinder.php b/system/ThirdParty/Kint/CallFinder.php index f85b1ae017a9..11c40ad85526 100644 --- a/system/ThirdParty/Kint/CallFinder.php +++ b/system/ThirdParty/Kint/CallFinder.php @@ -30,10 +30,17 @@ /** * @psalm-type PhpTokenArray = array{int, string, int} * @psalm-type PhpToken = string|PhpTokenArray + * @psalm-type CallParameter = array{ + * name: string, + * path: string, + * expression: bool, + * literal: bool, + * new_without_parens: bool, + * } */ class CallFinder { - private static $ignore = [ + private static array $ignore = [ T_CLOSE_TAG => true, T_COMMENT => true, T_DOC_COMMENT => true, @@ -49,13 +56,12 @@ class CallFinder * - Wrap the access path in parentheses if there * are any of these in the final short parameter. */ - private static $operator = [ + private static array $operator = [ T_AND_EQUAL => true, T_BOOLEAN_AND => true, T_BOOLEAN_OR => true, T_ARRAY_CAST => true, T_BOOL_CAST => true, - T_CLASS => true, T_CLONE => true, T_CONCAT_EQUAL => true, T_DEC => true, @@ -79,7 +85,6 @@ class CallFinder T_MINUS_EQUAL => true, T_MOD_EQUAL => true, T_MUL_EQUAL => true, - T_NEW => true, T_OBJECT_CAST => true, T_OR_EQUAL => true, T_PLUS_EQUAL => true, @@ -96,6 +101,8 @@ class CallFinder T_POW_EQUAL => true, T_SPACESHIP => true, T_DOUBLE_ARROW => true, + T_FN => true, + T_COALESCE_EQUAL => true, '!' => true, '%' => true, '&' => true, @@ -114,7 +121,12 @@ class CallFinder '~' => true, ]; - private static $strip = [ + private static array $preserve_spaces = [ + T_CLASS => true, + T_NEW => true, + ]; + + private static array $strip = [ '(' => true, ')' => true, '[' => true, @@ -126,19 +138,19 @@ class CallFinder T_NS_SEPARATOR => true, ]; - private static $classcalls = [ + private static array $classcalls = [ T_DOUBLE_COLON => true, T_OBJECT_OPERATOR => true, ]; - private static $namespace = [ + private static array $namespace = [ T_STRING => true, ]; /** * @psalm-param callable-array|callable-string $function * - * @psalm-return list}> + * @psalm-return list, modifiers: list}> * * @return array List of matching calls on the relevant line */ @@ -169,11 +181,6 @@ public static function getFunctionCalls(string $source, int $line, $function): a T_NS_SEPARATOR => true, ]; - if (KINT_PHP74) { - self::$operator[T_FN] = true; - self::$operator[T_COALESCE_EQUAL] = true; - } - if (KINT_PHP80) { $up[T_ATTRIBUTE] = true; self::$operator[T_MATCH] = true; @@ -187,9 +194,12 @@ public static function getFunctionCalls(string $source, int $line, $function): a $identifier[T_NAME_RELATIVE] = true; } + if (!KINT_PHP84) { + self::$operator[T_NEW] = true; // @codeCoverageIgnore + } + /** @psalm-var list */ $tokens = \token_get_all($source); - $cursor = 1; $function_calls = []; // Performance optimization preventing backwards loops @@ -204,6 +214,7 @@ public static function getFunctionCalls(string $source, int $line, $function): a $class = null; /** * @psalm-suppress RedundantFunctionCallGivenDocblockType + * Psalm bug #11075 */ $function = \strtolower($function); } @@ -214,11 +225,7 @@ public static function getFunctionCalls(string $source, int $line, $function): a continue; } - // Count newlines for line number instead of using $token[2] - // since certain situations (String tokens after whitespace) may - // not have the correct line number unless you do this manually - $cursor += \substr_count($token[1], "\n"); - if ($cursor > $line) { + if ($token[2] > $line) { break; } @@ -229,6 +236,12 @@ public static function getFunctionCalls(string $source, int $line, $function): a $prev_tokens = [$prev_tokens[1], $prev_tokens[2], $token]; + // The logic for 7.3 through 8.1 is far more complicated. + // This should speed things up without making a lot more work for us + if (KINT_PHP82 && $line !== $token[2]) { + continue; + } + // Check if it's the right type to be the function we're looking for if (!isset(self::$namespace[$token[0]])) { continue; @@ -242,26 +255,29 @@ public static function getFunctionCalls(string $source, int $line, $function): a // Check if it's a function call $nextReal = self::realTokenIndex($tokens, $index); - if (!isset($nextReal, $tokens[$nextReal]) || '(' !== $tokens[$nextReal]) { + if ('(' !== ($tokens[$nextReal] ?? null)) { continue; } // Check if it matches the signature if (null === $class) { - if ($prev_tokens[1] && isset(self::$classcalls[$prev_tokens[1][0]])) { + if (null !== $prev_tokens[1] && isset(self::$classcalls[$prev_tokens[1][0]])) { continue; } } else { - if (!$prev_tokens[1] || T_DOUBLE_COLON !== $prev_tokens[1][0]) { + if (null === $prev_tokens[1] || T_DOUBLE_COLON !== $prev_tokens[1][0]) { continue; } - if (!$prev_tokens[0] || !isset(self::$namespace[$prev_tokens[0][0]])) { + if (null === $prev_tokens[0] || !isset(self::$namespace[$prev_tokens[0][0]])) { continue; } // All self::$namespace tokens are T_ constants - /** @psalm-var PhpTokenArray $prev_tokens[0] */ + /** + * @psalm-var PhpTokenArray $prev_tokens[0] + * Psalm bug #746 (wontfix) + */ $ns = \explode('\\', \strtolower($prev_tokens[0][1])); if (\end($ns) !== $class) { @@ -269,7 +285,7 @@ public static function getFunctionCalls(string $source, int $line, $function): a } } - $inner_cursor = $cursor; + $last_line = $token[2]; $depth = 1; // The depth respective to the function call $offset = $nextReal + 1; // The start of the function call $instring = false; // Whether we're in a string or not @@ -283,10 +299,8 @@ public static function getFunctionCalls(string $source, int $line, $function): a while (isset($tokens[$offset])) { $token = $tokens[$offset]; - // Ensure that the $inner_cursor is correct and - // that $token is either a T_ constant or a string if (\is_array($token)) { - $inner_cursor += \substr_count($token[1], "\n"); + $last_line = $token[2]; } if (!isset(self::$ignore[$token[0]]) && !isset($down[$token[0]])) { @@ -312,7 +326,7 @@ public static function getFunctionCalls(string $source, int $line, $function): a } $shortparam[] = $token; } - } elseif ('"' === $token[0]) { + } elseif ('"' === $token || 'b"' === $token) { // Strings use the same symbol for up and down, but we can // only ever be inside one string, so just use a bool for that if ($instring) { @@ -326,7 +340,7 @@ public static function getFunctionCalls(string $source, int $line, $function): a $instring = !$instring; - $shortparam[] = '"'; + $shortparam[] = $token; } elseif (1 === $depth) { if (',' === $token[0]) { $params[] = [ @@ -336,8 +350,19 @@ public static function getFunctionCalls(string $source, int $line, $function): a $shortparam = []; $paramrealtokens = false; $param_start = $offset + 1; - } elseif (T_CONSTANT_ENCAPSED_STRING === $token[0] && \strlen($token[1]) > 2) { - $shortparam[] = $token[1][0].'...'.$token[1][0]; + } elseif (T_CONSTANT_ENCAPSED_STRING === $token[0]) { + $quote = $token[1][0]; + if ('b' === $quote) { + $quote = $token[1][1]; + if (\strlen($token[1]) > 3) { + $token[1] = 'b'.$quote.'...'.$quote; + } + } else { + if (\strlen($token[1]) > 2) { + $token[1] = $quote.'...'.$quote; + } + } + $shortparam[] = $token; } else { $shortparam[] = $token; } @@ -360,15 +385,21 @@ public static function getFunctionCalls(string $source, int $line, $function): a // If we're not passed (or at) the line at the end // of the function call, we're too early so skip it - if ($inner_cursor < $line) { - continue; + // Only applies to < 8.2 since we check line explicitly above that + if (!KINT_PHP82 && $last_line < $line) { + continue; // @codeCoverageIgnore } + $formatted_parameters = []; + // Format the final output parameters - foreach ($params as &$param) { + foreach ($params as $param) { $name = self::tokensFormatted($param['short']); - + $path = self::tokensToString(self::tokensTrim($param['full'])); $expression = false; + $literal = false; + $new_without_parens = false; + foreach ($name as $token) { if (self::tokenIsOperator($token)) { $expression = true; @@ -376,16 +407,79 @@ public static function getFunctionCalls(string $source, int $line, $function): a } } - $param = [ - 'name' => self::tokensToString($name), - 'path' => self::tokensToString(self::tokensTrim($param['full'])), + // As of 8.4 new is only an expression when parentheses are + // omitted. In that case we can cheat and add them ourselves. + // + // > PHP interprets the first expression after new as a class name + // per https://wiki.php.net/rfc/new_without_parentheses + if (KINT_PHP84 && !$expression && T_NEW === $name[0][0]) { + $had_name_token = false; + $new_without_parens = true; + + foreach ($name as $token) { + if (T_NEW === $token[0]) { + continue; + } + + if (isset(self::$ignore[$token[0]])) { + continue; + } + + if (T_CLASS === $token[0]) { + $new_without_parens = false; + break; + } + + if ('(' === $token && $had_name_token) { + $new_without_parens = false; + break; + } + + $had_name_token = true; + } + } + + if (!$expression && 1 === \count($name)) { + switch ($name[0][0]) { + case T_CONSTANT_ENCAPSED_STRING: + case T_LNUMBER: + case T_DNUMBER: + $literal = true; + break; + case T_STRING: + switch (\strtolower($name[0][1])) { + case 'null': + case 'true': + case 'false': + $literal = true; + } + } + + $name = self::tokensToString($name); + } else { + $name = self::tokensToString($name); + + if (!$expression) { + switch (\strtolower($name)) { + case 'array()': + case '[]': + $literal = true; + break; + } + } + } + + $formatted_parameters[] = [ + 'name' => $name, + 'path' => $path, 'expression' => $expression, + 'literal' => $literal, + 'new_without_parens' => $new_without_parens, ]; } // Skip first-class callables - /** @psalm-var list $params */ - if (KINT_PHP81 && 1 === \count($params) && '...' === \reset($params)['path']) { + if (KINT_PHP81 && 1 === \count($formatted_parameters) && '...' === \reset($formatted_parameters)['path']) { continue; } @@ -418,7 +512,7 @@ public static function getFunctionCalls(string $source, int $line, $function): a } $function_calls[] = [ - 'parameters' => $params, + 'parameters' => $formatted_parameters, 'modifiers' => $mods, ]; } @@ -426,9 +520,6 @@ public static function getFunctionCalls(string $source, int $line, $function): a return $function_calls; } - /** - * @psalm-param PhpToken[] $tokens - */ private static function realTokenIndex(array $tokens, int $index): ?int { ++$index; @@ -457,8 +548,13 @@ private static function tokenIsOperator($token): bool } /** - * @psalm-param PhpToken[] $tokens + * @psalm-param PhpToken $token The token to check */ + private static function tokenPreserveWhitespace($token): bool + { + return self::tokenIsOperator($token) || isset(self::$preserve_spaces[$token[0]]); + } + private static function tokensToString(array $tokens): string { $out = ''; @@ -474,9 +570,6 @@ private static function tokensToString(array $tokens): string return $out; } - /** - * @psalm-param PhpToken[] $tokens - */ private static function tokensTrim(array $tokens): array { foreach ($tokens as $index => $token) { @@ -500,11 +593,6 @@ private static function tokensTrim(array $tokens): array return \array_reverse($tokens); } - /** - * @psalm-param PhpToken[] $tokens - * - * @psalm-return PhpToken[] - */ private static function tokensFormatted(array $tokens): array { $tokens = self::tokensTrim($tokens); @@ -519,7 +607,7 @@ private static function tokensFormatted(array $tokens): array $last = null; if (T_FUNCTION === $tokens[0][0] || - (KINT_PHP74 && T_FN === $tokens[0][0]) || + T_FN === $tokens[0][0] || (KINT_PHP80 && T_MATCH === $tokens[0][0]) ) { $ignorestrip = true; @@ -538,21 +626,24 @@ private static function tokensFormatted(array $tokens): array } $next = $tokens[$next]; - /** @psalm-var PhpToken $last */ + /** + * @psalm-var PhpToken $last + * Since we call tokensTrim we know we can't be here without a $last + */ if ($attribute && ']' === $last[0]) { $attribute = false; - } elseif (!$ignorestrip && isset(self::$strip[$last[0]]) && !self::tokenIsOperator($next)) { + } elseif (!$ignorestrip && isset(self::$strip[$last[0]]) && !self::tokenPreserveWhitespace($next)) { continue; } - if (!$ignorestrip && isset(self::$strip[$next[0]]) && $last && !self::tokenIsOperator($last)) { + if (!$ignorestrip && isset(self::$strip[$next[0]]) && !self::tokenPreserveWhitespace($last)) { continue; } - $token = ' '; + $token[1] = ' '; $space = true; } else { - if (KINT_PHP80 && $last && T_ATTRIBUTE == $last[0]) { + if (KINT_PHP80 && null !== $last && T_ATTRIBUTE === $last[0]) { $attribute = true; } diff --git a/system/ThirdParty/Kint/FacadeInterface.php b/system/ThirdParty/Kint/FacadeInterface.php index da4badcd869a..d61f1bdb6e10 100644 --- a/system/ThirdParty/Kint/FacadeInterface.php +++ b/system/ThirdParty/Kint/FacadeInterface.php @@ -29,7 +29,7 @@ use Kint\Parser\Parser; use Kint\Renderer\RendererInterface; -use Kint\Zval\Value; +use Kint\Value\Context\ContextInterface; interface FacadeInterface { @@ -42,8 +42,8 @@ public function setStatesFromCallInfo(array $info): void; /** * Renders a list of vars including the pre and post renders. * - * @param array $vars Data to dump - * @param Value[] $base The base value objects + * @param array $vars Data to dump + * @param ContextInterface[] $base The base contexts */ public function dumpAll(array $vars, array $base): string; } diff --git a/system/ThirdParty/Kint/Kint.php b/system/ThirdParty/Kint/Kint.php index 418c1f3f8789..6c55593d4ab2 100644 --- a/system/ThirdParty/Kint/Kint.php +++ b/system/ThirdParty/Kint/Kint.php @@ -31,12 +31,22 @@ use Kint\Parser\ConstructablePluginInterface; use Kint\Parser\Parser; use Kint\Parser\PluginInterface; +use Kint\Renderer\ConstructableRendererInterface; use Kint\Renderer\RendererInterface; use Kint\Renderer\TextRenderer; -use Kint\Zval\Value; +use Kint\Value\Context\BaseContext; +use Kint\Value\Context\ContextInterface; +use Kint\Value\UninitializedValue; /** * @psalm-consistent-constructor + * Psalm bug #8523 + * + * @psalm-import-type CallParameter from CallFinder + * + * @psalm-type KintMode = Kint::MODE_*|bool + * + * @psalm-api */ class Kint implements FacadeInterface { @@ -51,134 +61,111 @@ class Kint implements FacadeInterface * false: Disabled * true: Enabled, default mode selection * other: Manual mode selection + * + * @psalm-var KintMode */ public static $enabled_mode = true; /** * Default mode. * - * @var string + * @psalm-var KintMode */ public static $mode_default = self::MODE_RICH; /** * Default mode in CLI with cli_detection on. * - * @var string + * @psalm-var KintMode */ public static $mode_default_cli = self::MODE_CLI; /** - * @var bool Return output instead of echoing - */ - public static $return; - - /** - * @var string format of the link to the source file in trace entries. - * - * Use %f for file path, %l for line number. - * - * [!] EXAMPLE (works with for phpStorm and RemoteCall Plugin): + * @var bool enable detection when Kint is command line. * - * Kint::$file_link_format = 'http://localhost:8091/?message=%f:%l'; - */ - public static $file_link_format = ''; - - /** - * @var bool whether to display where kint was called from + * Formats output with whitespace only; does not HTML-escape it */ - public static $display_called_from = true; + public static bool $cli_detection = true; /** - * @var array base directories of your application that will be displayed instead of the full path. - * - * Keys are paths, values are replacement strings - * - * [!] EXAMPLE (for Laravel 5): - * - * Kint::$app_root_dirs = [ - * base_path() => '', - * app_path() => '', - * config_path() => '', - * database_path() => '', - * public_path() => '', - * resource_path() => '', - * storage_path() => '', - * ]; - * - * Defaults to [$_SERVER['DOCUMENT_ROOT'] => ''] + * @var bool Return output instead of echoing */ - public static $app_root_dirs = []; + public static bool $return = false; /** * @var int depth limit for array/object traversal. 0 for no limit */ - public static $depth_limit = 7; + public static int $depth_limit = 7; /** * @var bool expand all trees by default for rich view */ - public static $expanded = false; + public static bool $expanded = false; /** - * @var bool enable detection when Kint is command line. - * - * Formats output with whitespace only; does not HTML-escape it + * @var bool whether to display where kint was called from */ - public static $cli_detection = true; + public static bool $display_called_from = true; /** * @var array Kint aliases. Add debug functions in Kint wrappers here to fix modifiers and backtraces */ - public static $aliases = [ - ['Kint\\Kint', 'dump'], - ['Kint\\Kint', 'trace'], - ['Kint\\Kint', 'dumpAll'], + public static array $aliases = [ + [self::class, 'dump'], + [self::class, 'trace'], + [self::class, 'dumpAll'], ]; /** - * @psalm-var class-string[] Array of modes to renderer class names + * @psalm-var array> + * + * Array of modes to renderer class names */ - public static $renderers = [ - self::MODE_RICH => \Kint\Renderer\RichRenderer::class, - self::MODE_PLAIN => \Kint\Renderer\PlainRenderer::class, - self::MODE_TEXT => \Kint\Renderer\TextRenderer::class, - self::MODE_CLI => \Kint\Renderer\CliRenderer::class, + public static array $renderers = [ + self::MODE_RICH => Renderer\RichRenderer::class, + self::MODE_PLAIN => Renderer\PlainRenderer::class, + self::MODE_TEXT => TextRenderer::class, + self::MODE_CLI => Renderer\CliRenderer::class, ]; /** - * @psalm-var class-string[] + * @psalm-var array> */ - public static $plugins = [ + public static array $plugins = [ \Kint\Parser\ArrayLimitPlugin::class, \Kint\Parser\ArrayObjectPlugin::class, \Kint\Parser\Base64Plugin::class, + \Kint\Parser\BinaryPlugin::class, \Kint\Parser\BlacklistPlugin::class, + \Kint\Parser\ClassHooksPlugin::class, \Kint\Parser\ClassMethodsPlugin::class, \Kint\Parser\ClassStaticsPlugin::class, + \Kint\Parser\ClassStringsPlugin::class, \Kint\Parser\ClosurePlugin::class, \Kint\Parser\ColorPlugin::class, \Kint\Parser\DateTimePlugin::class, + \Kint\Parser\DomPlugin::class, \Kint\Parser\EnumPlugin::class, \Kint\Parser\FsPathPlugin::class, + \Kint\Parser\HtmlPlugin::class, \Kint\Parser\IteratorPlugin::class, \Kint\Parser\JsonPlugin::class, \Kint\Parser\MicrotimePlugin::class, + \Kint\Parser\MysqliPlugin::class, + // \Kint\Parser\SerializePlugin::class, \Kint\Parser\SimpleXMLElementPlugin::class, \Kint\Parser\SplFileInfoPlugin::class, - \Kint\Parser\SplObjectStoragePlugin::class, \Kint\Parser\StreamPlugin::class, \Kint\Parser\TablePlugin::class, \Kint\Parser\ThrowablePlugin::class, \Kint\Parser\TimestampPlugin::class, + \Kint\Parser\ToStringPlugin::class, \Kint\Parser\TracePlugin::class, \Kint\Parser\XmlPlugin::class, ]; - protected static $plugin_pool = []; - - protected $parser; - protected $renderer; + protected Parser $parser; + protected RendererInterface $renderer; public function __construct(Parser $p, RendererInterface $r) { @@ -210,7 +197,7 @@ public function setStatesFromStatics(array $statics): void { $this->renderer->setStatics($statics); - $this->parser->setDepthLimit(isset($statics['depth_limit']) ? $statics['depth_limit'] : 0); + $this->parser->setDepthLimit($statics['depth_limit'] ?? 0); $this->parser->clearPlugins(); if (!isset($statics['plugins'])) { @@ -222,19 +209,22 @@ public function setStatesFromStatics(array $statics): void foreach ($statics['plugins'] as $plugin) { if ($plugin instanceof PluginInterface) { $plugins[] = $plugin; - } elseif (\is_string($plugin) && \is_subclass_of($plugin, ConstructablePluginInterface::class)) { - if (!isset(static::$plugin_pool[$plugin])) { - $p = new $plugin(); - static::$plugin_pool[$plugin] = $p; - } - $plugins[] = static::$plugin_pool[$plugin]; + } elseif (\is_string($plugin) && \is_a($plugin, ConstructablePluginInterface::class, true)) { + $plugins[] = new $plugin($this->parser); } } $plugins = $this->renderer->filterParserPlugins($plugins); foreach ($plugins as $plugin) { - $this->parser->addPlugin($plugin); + try { + $this->parser->addPlugin($plugin); + } catch (InvalidArgumentException $e) { + \trigger_error( + 'Plugin '.Utils::errorSanitizeString(\get_class($plugin)).' could not be added to a Kint parser: '.Utils::errorSanitizeString($e->getMessage()), + E_USER_WARNING + ); + } } } @@ -246,7 +236,7 @@ public function setStatesFromCallInfo(array $info): void $this->parser->setDepthLimit(0); } - $this->parser->setCallerClass(isset($info['caller']['class']) ? $info['caller']['class'] : null); + $this->parser->setCallerClass($info['caller']['class'] ?? null); } public function dumpAll(array $vars, array $base): string @@ -255,17 +245,17 @@ public function dumpAll(array $vars, array $base): string throw new InvalidArgumentException('Kint::dumpAll requires arrays of identical size and keys as arguments'); } - $output = $this->renderer->preRender(); - if ([] === $vars) { - $output .= $this->renderer->renderNothing(); + return $this->dumpNothing(); } - foreach ($vars as $key => $arg) { - if (!$base[$key] instanceof Value) { - throw new InvalidArgumentException('Kint::dumpAll requires all elements of the second argument to be Value instances'); + $output = $this->renderer->preRender(); + + foreach ($vars as $key => $_) { + if (!$base[$key] instanceof ContextInterface) { + throw new InvalidArgumentException('Kint::dumpAll requires all elements of the second argument to be ContextInterface instances'); } - $output .= $this->dumpVar($arg, $base[$key]); + $output .= $this->dumpVar($vars[$key], $base[$key]); } $output .= $this->renderer->postRender(); @@ -273,16 +263,24 @@ public function dumpAll(array $vars, array $base): string return $output; } + protected function dumpNothing(): string + { + $output = $this->renderer->preRender(); + $output .= $this->renderer->render(new UninitializedValue(new BaseContext('No argument'))); + $output .= $this->renderer->postRender(); + + return $output; + } + /** * Dumps and renders a var. * * @param mixed &$var Data to dump - * @param Value $base Base object */ - protected function dumpVar(&$var, Value $base): string + protected function dumpVar(&$var, ContextInterface $c): string { return $this->renderer->render( - $this->parser->parse($var, $base) + $this->parser->parse($var, $c) ); } @@ -295,13 +293,11 @@ public static function getStatics(): array { return [ 'aliases' => static::$aliases, - 'app_root_dirs' => static::$app_root_dirs, 'cli_detection' => static::$cli_detection, 'depth_limit' => static::$depth_limit, 'display_called_from' => static::$display_called_from, 'enabled_mode' => static::$enabled_mode, 'expanded' => static::$expanded, - 'file_link_format' => static::$file_link_format, 'mode_default' => static::$mode_default, 'mode_default_cli' => static::$mode_default_cli, 'plugins' => static::$plugins, @@ -335,67 +331,57 @@ public static function createFromStatics(array $statics): ?FacadeInterface return null; } - /** @psalm-var class-string[] $statics['renderers'] */ - if (isset($statics['renderers'][$mode]) && \is_subclass_of($statics['renderers'][$mode], RendererInterface::class)) { - $renderer = new $statics['renderers'][$mode](); - } else { - $renderer = new TextRenderer(); + $renderer = null; + if (isset($statics['renderers'][$mode])) { + if ($statics['renderers'][$mode] instanceof RendererInterface) { + $renderer = $statics['renderers'][$mode]; + } + + if (\is_a($statics['renderers'][$mode], ConstructableRendererInterface::class, true)) { + $renderer = new $statics['renderers'][$mode](); + } } + $renderer ??= new TextRenderer(); + return new static(new Parser(), $renderer); } /** - * Creates base objects given parameter info. + * Creates base contexts given parameter info. * - * @param array $params Parameters as returned from getCallInfo - * @param int $argc Number of arguments the helper was called with + * @psalm-param list $params * - * @return Value[] Base objects for the arguments + * @return BaseContext[] Base contexts for the arguments */ public static function getBasesFromParamInfo(array $params, int $argc): array { - static $blacklist = [ - 'null', - 'true', - 'false', - 'array(...)', - 'array()', - '[...]', - '[]', - '(...)', - '()', - '"..."', - 'b"..."', - "'...'", - "b'...'", - ]; - - $params = \array_values($params); $bases = []; for ($i = 0; $i < $argc; ++$i) { $param = $params[$i] ?? null; - if (!isset($param['name']) || \is_numeric($param['name'])) { - $name = null; - } elseif (\in_array(\strtolower($param['name']), $blacklist, true)) { - $name = null; + if (!empty($param['literal'])) { + $name = 'literal'; } else { - $name = $param['name']; + $name = $param['name'] ?? '$'.$i; } if (isset($param['path'])) { $access_path = $param['path']; - if (!empty($param['expression'])) { + if ($param['expression']) { $access_path = '('.$access_path.')'; + } elseif ($param['new_without_parens']) { + $access_path .= '()'; } } else { $access_path = '$'.$i; } - $bases[] = Value::blank($name, $access_path); + $base = new BaseContext($name); + $base->access_path = $access_path; + $bases[] = $base; } return $bases; @@ -411,6 +397,8 @@ public static function getBasesFromParamInfo(array $params, int $argc): array * @param array $args Arguments * * @return array Call info + * + * @psalm-param list $trace */ public static function getCallInfo(array $aliases, array $trace, array $args): array { @@ -419,7 +407,7 @@ public static function getCallInfo(array $aliases, array $trace, array $args): a $caller = null; $miniTrace = []; - foreach ($trace as $index => $frame) { + foreach ($trace as $frame) { if (Utils::traceFrameIsListed($frame, $aliases)) { $found = true; $miniTrace = []; @@ -456,7 +444,7 @@ public static function getCallInfo(array $aliases, array $trace, array $args): a 'trace' => $miniTrace, ]; - if ($call) { + if (null !== $call) { $ret['params'] = $call['parameters']; $ret['modifiers'] = $call['modifiers']; } @@ -477,7 +465,7 @@ public static function trace() return 0; } - Utils::normalizeAliases(static::$aliases); + static::$aliases = Utils::normalizeAliases(static::$aliases); $call_info = static::getCallInfo(static::$aliases, \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), []); @@ -514,10 +502,9 @@ public static function trace() \array_shift($trimmed_trace); - $output = $kintstance->dumpAll( - [$trimmed_trace], - [Value::blank('Kint\\Kint::trace()', 'debug_backtrace()')] - ); + $base = new BaseContext('Kint\\Kint::trace()'); + $base->access_path = 'debug_backtrace()'; + $output = $kintstance->dumpAll([$trimmed_trace], [$base]); if (static::$return || \in_array('@', $call_info['modifiers'], true)) { return $output; @@ -537,7 +524,7 @@ public static function trace() * * Functionally equivalent to Kint::dump(1) or Kint::dump(debug_backtrace()) * - * @psalm-param array ...$args + * @psalm-param mixed ...$args * * @return int|string */ @@ -547,7 +534,7 @@ public static function dump(...$args) return 0; } - Utils::normalizeAliases(static::$aliases); + static::$aliases = Utils::normalizeAliases(static::$aliases); $call_info = static::getCallInfo(static::$aliases, \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), $args); @@ -587,54 +574,6 @@ public static function dump(...$args) return 0; } - /** - * generic path display callback, can be configured in app_root_dirs; purpose is - * to show relevant path info and hide as much of the path as possible. - */ - public static function shortenPath(string $file): string - { - $file = \array_values(\array_filter(\explode('/', \str_replace('\\', '/', $file)), 'strlen')); - - $longest_match = 0; - $match = '/'; - - foreach (static::$app_root_dirs as $path => $alias) { - /** @psalm-var string $path */ - if (empty($path)) { - continue; - } - - $path = \array_values(\array_filter(\explode('/', \str_replace('\\', '/', $path)), 'strlen')); - - if (\array_slice($file, 0, \count($path)) === $path && \count($path) > $longest_match) { - $longest_match = \count($path); - $match = $alias; - } - } - - if ($longest_match) { - $file = \array_merge([$match], \array_slice($file, $longest_match)); - - return \implode('/', $file); - } - - // fallback to find common path with Kint dir - $kint = \array_values(\array_filter(\explode('/', \str_replace('\\', '/', KINT_DIR)), 'strlen')); - - foreach ($file as $i => $part) { - if (!isset($kint[$i]) || $kint[$i] !== $part) { - return ($i ? '.../' : '/').\implode('/', \array_slice($file, $i)); - } - } - - return '/'.\implode('/', $file); - } - - public static function getIdeLink(string $file, int $line): string - { - return \str_replace(['%f', '%l'], [$file, $line], static::$file_link_format); - } - /** * Returns specific function call info from a stack trace frame, or null if no match could be found. * @@ -648,7 +587,7 @@ protected static function getSingleCall(array $frame, array $args): ?array if ( !isset($frame['file'], $frame['line'], $frame['function']) || !\is_readable($frame['file']) || - !$source = \file_get_contents($frame['file']) + false === ($source = \file_get_contents($frame['file'])) ) { return null; } @@ -686,6 +625,8 @@ protected static function getSingleCall(array $frame, array $args): ?array 'name' => \substr($param['name'], 3).'['.\var_export($key, true).']', 'path' => \substr($param['path'], 3).'['.\var_export($key, true).']', 'expression' => false, + 'literal' => false, + 'new_without_parens' => false, ]; } } else { @@ -696,6 +637,8 @@ protected static function getSingleCall(array $frame, array $args): ?array 'name' => 'array_values('.\substr($param['name'], 3).')['.$j.']', 'path' => 'array_values('.\substr($param['path'], 3).')['.$j.']', 'expression' => false, + 'literal' => false, + 'new_without_parens' => false, ]; } } diff --git a/system/ThirdParty/Kint/Parser/AbstractPlugin.php b/system/ThirdParty/Kint/Parser/AbstractPlugin.php index a3f896843a3a..6ab031d0b586 100644 --- a/system/ThirdParty/Kint/Parser/AbstractPlugin.php +++ b/system/ThirdParty/Kint/Parser/AbstractPlugin.php @@ -27,19 +27,22 @@ namespace Kint\Parser; -/** - * @psalm-consistent-constructor - */ abstract class AbstractPlugin implements ConstructablePluginInterface { - protected $parser; + private Parser $parser; - public function __construct() + public function __construct(Parser $parser) { + $this->parser = $parser; } public function setParser(Parser $p): void { $this->parser = $p; } + + protected function getParser(): Parser + { + return $this->parser; + } } diff --git a/system/ThirdParty/Kint/Parser/ArrayLimitPlugin.php b/system/ThirdParty/Kint/Parser/ArrayLimitPlugin.php index 2e7fca93842d..bd54f5fc235d 100644 --- a/system/ThirdParty/Kint/Parser/ArrayLimitPlugin.php +++ b/system/ThirdParty/Kint/Parser/ArrayLimitPlugin.php @@ -29,30 +29,43 @@ use InvalidArgumentException; use Kint\Utils; -use Kint\Zval\Value; - -class ArrayLimitPlugin extends AbstractPlugin +use Kint\Value\AbstractValue; +use Kint\Value\ArrayValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\Context\ContextInterface; +use Kint\Value\Representation\ContainerRepresentation; +use Kint\Value\Representation\ProfileRepresentation; +use Kint\Value\Representation\ValueRepresentation; + +class ArrayLimitPlugin extends AbstractPlugin implements PluginBeginInterface { /** * Maximum size of arrays before limiting. - * - * @var int */ - public static $trigger = 1000; + public static int $trigger = 1000; /** * Maximum amount of items to show in a limited array. - * - * @var int */ - public static $limit = 50; + public static int $limit = 50; /** * Don't limit arrays with string keys. - * - * @var bool */ - public static $numeric_only = true; + public static bool $numeric_only = true; + + public function __construct(Parser $p) + { + if (self::$limit < 0) { + throw new InvalidArgumentException('ArrayLimitPlugin::$limit can not be lower than 0'); + } + + if (self::$limit >= self::$trigger) { + throw new InvalidArgumentException('ArrayLimitPlugin::$limit can not be lower than ArrayLimitPlugin::$trigger'); + } + + parent::__construct($p); + } public function getTypes(): array { @@ -64,80 +77,92 @@ public function getTriggers(): int return Parser::TRIGGER_BEGIN; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseBegin(&$var, ContextInterface $c): ?AbstractValue { - if (self::$limit >= self::$trigger) { - throw new InvalidArgumentException('ArrayLimitPlugin::$limit can not be lower than ArrayLimitPlugin::$trigger'); - } - - $depth = $this->parser->getDepthLimit(); + $parser = $this->getParser(); + $pdepth = $parser->getDepthLimit(); - if (!$depth) { - return; + if (!$pdepth) { + return null; } - if ($o->depth >= $depth - 1) { - return; + $cdepth = $c->getDepth(); + + if ($cdepth >= $pdepth - 1) { + return null; } if (\count($var) < self::$trigger) { - return; + return null; } if (self::$numeric_only && Utils::isAssoc($var)) { - return; + return null; } - $base = clone $o; - $base->depth = $depth - 1; - $obj = $this->parser->parse($var, $base); + $slice = \array_slice($var, 0, self::$limit, true); + $array = $parser->parse($slice, $c); - if ('array' != $obj->type) { - return; // @codeCoverageIgnore + if (!$array instanceof ArrayValue) { + return null; } - $obj->depth = $o->depth; - $i = 0; + $base = new BaseContext($c->getName()); + $base->depth = $pdepth - 1; + $base->access_path = $c->getAccessPath(); - foreach ($obj->value->contents as $child) { - // We only bother setting the correct depth for the first child, - // any deeper children should be cancelled by the depth limit - $child->depth = $o->depth + 1; - $this->recalcDepthLimit($child); + $slice = \array_slice($var, self::$limit, null, true); + $slice = $parser->parse($slice, $base); + + if (!$slice instanceof ArrayValue) { + return null; } - $var2 = \array_slice($var, 0, self::$limit, true); - $base = clone $o; - $slice = $this->parser->parse($var2, $base); + foreach ($slice->getContents() as $child) { + $this->replaceDepthLimit($child, $cdepth + 1); + } - \array_splice($obj->value->contents, 0, self::$limit, $slice->value->contents); + $out = new ArrayValue($c, \count($var), \array_merge($array->getContents(), $slice->getContents())); + $out->flags = $array->flags; - $o = $obj; + // Explicitly copy over profile plugin + $arrayp = $array->getRepresentation('profiling'); + $slicep = $slice->getRepresentation('profiling'); + if ($arrayp instanceof ProfileRepresentation && $slicep instanceof ProfileRepresentation) { + $out->addRepresentation(new ProfileRepresentation($arrayp->complexity + $slicep->complexity)); + } - $this->parser->haltParse(); + // Add contents. Check is in case some bad plugin empties both $slice and $array + if ($contents = $out->getContents()) { + $out->addRepresentation(new ContainerRepresentation('Contents', $contents, null, true)); + } + + return $out; } - protected function recalcDepthLimit(Value $o): void + protected function replaceDepthLimit(AbstractValue $v, int $depth): void { - $hintkey = \array_search('depth_limit', $o->hints, true); - if (false !== $hintkey) { - $o->hints[$hintkey] = 'array_limit'; + $c = $v->getContext(); + + if ($c instanceof BaseContext) { + $c->depth = $depth; } - $reps = $o->getRepresentations(); - if ($o->value) { - $reps[] = $o->value; + $pdepth = $this->getParser()->getDepthLimit(); + + if (($v->flags & AbstractValue::FLAG_DEPTH_LIMIT) && $pdepth && $depth < $pdepth) { + $v->flags = $v->flags & ~AbstractValue::FLAG_DEPTH_LIMIT | AbstractValue::FLAG_ARRAY_LIMIT; } + $reps = $v->getRepresentations(); + foreach ($reps as $rep) { - if ($rep->contents instanceof Value) { - $this->recalcDepthLimit($rep->contents); - } elseif (\is_array($rep->contents)) { - foreach ($rep->contents as $child) { - if ($child instanceof Value) { - $this->recalcDepthLimit($child); - } + if ($rep instanceof ContainerRepresentation) { + foreach ($rep->getContents() as $child) { + $this->replaceDepthLimit($child, $depth + 1); } + } elseif ($rep instanceof ValueRepresentation) { + $this->replaceDepthLimit($rep->getValue(), $depth + 1); } } } diff --git a/system/ThirdParty/Kint/Parser/ArrayObjectPlugin.php b/system/ThirdParty/Kint/Parser/ArrayObjectPlugin.php index 82ff7593e3b7..605d695a887b 100644 --- a/system/ThirdParty/Kint/Parser/ArrayObjectPlugin.php +++ b/system/ThirdParty/Kint/Parser/ArrayObjectPlugin.php @@ -28,9 +28,10 @@ namespace Kint\Parser; use ArrayObject; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\ContextInterface; -class ArrayObjectPlugin extends AbstractPlugin +class ArrayObjectPlugin extends AbstractPlugin implements PluginBeginInterface { public function getTypes(): array { @@ -42,24 +43,26 @@ public function getTriggers(): int return Parser::TRIGGER_BEGIN; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseBegin(&$var, ContextInterface $c): ?AbstractValue { if (!$var instanceof ArrayObject) { - return; + return null; } $flags = $var->getFlags(); if (ArrayObject::STD_PROP_LIST === $flags) { - return; + return null; } + $parser = $this->getParser(); + $var->setFlags(ArrayObject::STD_PROP_LIST); - $o = $this->parser->parse($var, $o); + $v = $parser->parse($var, $c); $var->setFlags($flags); - $this->parser->haltParse(); + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/Base64Plugin.php b/system/ThirdParty/Kint/Parser/Base64Plugin.php index a25a7343ac1f..9cc6eee94c93 100644 --- a/system/ThirdParty/Kint/Parser/Base64Plugin.php +++ b/system/ThirdParty/Kint/Parser/Base64Plugin.php @@ -27,24 +27,22 @@ namespace Kint\Parser; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\Representation\ValueRepresentation; +use Kint\Value\StringValue; -class Base64Plugin extends AbstractPlugin +class Base64Plugin extends AbstractPlugin implements PluginCompleteInterface { /** * The minimum length before a string will be considered for base64 decoding. - * - * @var int */ - public static $min_length_hard = 16; + public static int $min_length_hard = 16; /** * The minimum length before the base64 decoding will take precedence. - * - * @var int */ - public static $min_length_soft = 50; + public static int $min_length_soft = 50; public function getTypes(): array { @@ -56,41 +54,50 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if (\strlen($var) < self::$min_length_hard || \strlen($var) % 4) { - return; + return $v; } if (\preg_match('/^[A-Fa-f0-9]+$/', $var)) { - return; + return $v; } if (!\preg_match('/^[A-Za-z0-9+\\/=]+$/', $var)) { - return; + return $v; } $data = \base64_decode($var, true); if (false === $data) { - return; + return $v; } - $base_obj = new Value(); - $base_obj->depth = $o->depth + 1; - $base_obj->name = 'base64_decode('.$o->name.')'; + $c = $v->getContext(); - if ($o->access_path) { - $base_obj->access_path = 'base64_decode('.$o->access_path.')'; + $base = new BaseContext('base64_decode('.$c->getName().')'); + $base->depth = $c->getDepth() + 1; + + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = 'base64_decode('.$ap.')'; } - $r = new Representation('Base64'); - $r->contents = $this->parser->parse($data, $base_obj); + $data = $this->getParser()->parse($data, $base); + $data->flags |= AbstractValue::FLAG_GENERATED; + + if (!$data instanceof StringValue || false === $data->getEncoding()) { + return $v; + } + + $r = new ValueRepresentation('Base64', $data); if (\strlen($var) > self::$min_length_soft) { - $o->addRepresentation($r, 0); + $v->addRepresentation($r, 0); } else { - $o->addRepresentation($r); + $v->addRepresentation($r); } + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/BinaryPlugin.php b/system/ThirdParty/Kint/Parser/BinaryPlugin.php index 56cf68a3359a..0ee1f639ab6e 100644 --- a/system/ThirdParty/Kint/Parser/BinaryPlugin.php +++ b/system/ThirdParty/Kint/Parser/BinaryPlugin.php @@ -27,10 +27,11 @@ namespace Kint\Parser; -use Kint\Zval\BlobValue; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Representation\BinaryRepresentation; +use Kint\Value\StringValue; -class BinaryPlugin extends AbstractPlugin +class BinaryPlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -42,10 +43,12 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$o instanceof BlobValue || !\in_array($o->encoding, ['ASCII', 'UTF-8'], true)) { - $o->value->hints[] = 'binary'; + if ($v instanceof StringValue && false === $v->getEncoding()) { + $v->addRepresentation(new BinaryRepresentation($v->getValue(), true), 0); } + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/BlacklistPlugin.php b/system/ThirdParty/Kint/Parser/BlacklistPlugin.php index fa54a3a6743e..2fa4fed56137 100644 --- a/system/ThirdParty/Kint/Parser/BlacklistPlugin.php +++ b/system/ThirdParty/Kint/Parser/BlacklistPlugin.php @@ -27,25 +27,30 @@ namespace Kint\Parser; -use Kint\Zval\InstanceValue; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\ContextInterface; +use Kint\Value\InstanceValue; use Psr\Container\ContainerInterface; +use Psr\EventDispatcher\EventDispatcherInterface; -class BlacklistPlugin extends AbstractPlugin +class BlacklistPlugin extends AbstractPlugin implements PluginBeginInterface { /** * List of classes and interfaces to blacklist. * - * @var array + * @var class-string[] */ - public static $blacklist = []; + public static array $blacklist = []; /** * List of classes and interfaces to blacklist except when dumped directly. * - * @var array + * @var class-string[] */ - public static $shallow_blacklist = [ContainerInterface::class]; + public static array $shallow_blacklist = [ + ContainerInterface::class, + EventDispatcherInterface::class, + ]; public function getTypes(): array { @@ -57,45 +62,35 @@ public function getTriggers(): int return Parser::TRIGGER_BEGIN; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseBegin(&$var, ContextInterface $c): ?AbstractValue { foreach (self::$blacklist as $class) { if ($var instanceof $class) { - $this->blacklistValue($var, $o); - - return; + return $this->blacklistValue($var, $c); } } - if ($o->depth <= 0) { - return; + if ($c->getDepth() <= 0) { + return null; } foreach (self::$shallow_blacklist as $class) { if ($var instanceof $class) { - $this->blacklistValue($var, $o); - - return; + return $this->blacklistValue($var, $c); } } + + return null; } /** * @param object &$var */ - protected function blacklistValue(&$var, Value &$o): void + protected function blacklistValue(&$var, ContextInterface $c): InstanceValue { - $object = new InstanceValue(); - $object->transplant($o); - $object->classname = \get_class($var); - $object->spl_object_hash = \spl_object_hash($var); - $object->clearRepresentations(); - $object->value = null; - $object->size = null; - $object->hints[] = 'blacklist'; - - $o = $object; + $object = new InstanceValue($c, \get_class($var), \spl_object_hash($var), \spl_object_id($var)); + $object->flags |= AbstractValue::FLAG_BLACKLIST; - $this->parser->haltParse(); + return $object; } } diff --git a/system/ThirdParty/Kint/Parser/ClassHooksPlugin.php b/system/ThirdParty/Kint/Parser/ClassHooksPlugin.php new file mode 100644 index 000000000000..1d658bfa909d --- /dev/null +++ b/system/ThirdParty/Kint/Parser/ClassHooksPlugin.php @@ -0,0 +1,122 @@ +> */ + private array $cache = []; + /** @psalm-var array> */ + private array $cache_verbose = []; + + public function getTypes(): array + { + return ['object']; + } + + public function getTriggers(): int + { + if (!KINT_PHP84) { + return Parser::TRIGGER_NONE; // @codeCoverageIgnore + } + + return Parser::TRIGGER_SUCCESS; + } + + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue + { + if (!$v instanceof InstanceValue) { + return $v; + } + + $props = $v->getRepresentation('properties'); + + if (!$props instanceof ContainerRepresentation) { + return $v; + } + + foreach ($props->getContents() as $prop) { + $c = $prop->getContext(); + + if (!$c instanceof PropertyContext || PropertyContext::HOOK_NONE === $c->hooks) { + continue; + } + + $cname = $c->getName(); + $cowner = $c->owner_class; + + if (!isset($this->cache_verbose[$cowner][$cname])) { + $ref = new ReflectionProperty($cowner, $cname); + $hooks = $ref->getHooks(); + + foreach ($hooks as $hook) { + if (!self::$verbose && false === $hook->getDocComment()) { + continue; + } + + $m = new MethodValue( + new MethodContext($hook), + new DeclaredCallableBag($hook) + ); + + $this->cache_verbose[$cowner][$cname][] = $m; + + if (false !== $hook->getDocComment()) { + $this->cache[$cowner][$cname][] = $m; + } + } + + $this->cache[$cowner][$cname] ??= []; + + if (self::$verbose) { + $this->cache_verbose[$cowner][$cname] ??= []; + } + } + + $cache = self::$verbose ? $this->cache_verbose : $this->cache; + $cache = $cache[$cowner][$cname] ?? []; + + if (\count($cache)) { + $prop->addRepresentation(new ContainerRepresentation('Hooks', $cache, 'propertyhooks')); + } + } + + return $v; + } +} diff --git a/system/ThirdParty/Kint/Parser/ClassMethodsPlugin.php b/system/ThirdParty/Kint/Parser/ClassMethodsPlugin.php index e71d537aa838..e06df2b61042 100644 --- a/system/ThirdParty/Kint/Parser/ClassMethodsPlugin.php +++ b/system/ThirdParty/Kint/Parser/ClassMethodsPlugin.php @@ -27,15 +27,32 @@ namespace Kint\Parser; -use Kint\Zval\InstanceValue; -use Kint\Zval\MethodValue; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\MethodContext; +use Kint\Value\DeclaredCallableBag; +use Kint\Value\InstanceValue; +use Kint\Value\MethodValue; +use Kint\Value\Representation\ContainerRepresentation; use ReflectionClass; +use ReflectionMethod; -class ClassMethodsPlugin extends AbstractPlugin +class ClassMethodsPlugin extends AbstractPlugin implements PluginCompleteInterface { - private static $cache = []; + public static bool $show_access_path = true; + + /** + * Whether to go out of the way to show constructor paths + * when the instance isn't accessible. + * + * Disabling this improves performance. + */ + public static bool $show_constructor_path = false; + + /** @psalm-var array */ + private array $instance_cache = []; + + /** @psalm-var array */ + private array $static_cache = []; public function getTypes(): array { @@ -47,69 +64,162 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + /** + * @psalm-template T of AbstractValue + * + * @psalm-param mixed $var + * @psalm-param T $v + * + * @psalm-return T + */ + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - $class = \get_class($var); - - // assuming class definition will not change inside one request - if (!isset(self::$cache[$class])) { - $methods = []; - - $reflection = new ReflectionClass($class); + if (!$v instanceof InstanceValue) { + return $v; + } - foreach ($reflection->getMethods() as $method) { - $methods[] = new MethodValue($method); + $class = $v->getClassName(); + $scope = $this->getParser()->getCallerClass(); + + if ($contents = $this->getCachedMethods($class)) { + if (self::$show_access_path) { + if (null !== $v->getContext()->getAccessPath()) { + // If we have an access path we can generate them for the children + foreach ($contents as $key => $val) { + if ($val->getContext()->isAccessible($scope)) { + $val = clone $val; + $val->getContext()->setAccessPathFromParent($v); + $contents[$key] = $val; + } + } + } elseif (self::$show_constructor_path && isset($contents['__construct'])) { + // __construct is the only exception: The only non-static method + // that can be called without access to the parent instance. + // Technically I guess it really is a static method but so long + // as PHP continues to refer to it as a normal one so will we. + $val = $contents['__construct']; + if ($val->getContext()->isAccessible($scope)) { + $val = clone $val; + $val->getContext()->setAccessPathFromParent($v); + $contents['__construct'] = $val; + } + } } - \usort($methods, ['Kint\\Parser\\ClassMethodsPlugin', 'sort']); + $v->addRepresentation(new ContainerRepresentation('Methods', $contents)); + } - self::$cache[$class] = $methods; + if ($contents = $this->getCachedStaticMethods($class)) { + $v->addRepresentation(new ContainerRepresentation('Static methods', $contents)); } - if (!empty(self::$cache[$class])) { - $rep = new Representation('Available methods', 'methods'); + return $v; + } - // Can't cache access paths - foreach (self::$cache[$class] as $m) { - $method = clone $m; - $method->depth = $o->depth + 1; + /** + * @psalm-param class-string $class + * + * @psalm-return MethodValue[] + */ + private function getCachedMethods(string $class): array + { + if (!isset($this->instance_cache[$class])) { + $methods = []; - if (!$this->parser->childHasPath($o, $method)) { - $method->access_path = null; - } else { - $method->setAccessPathFrom($o); + $r = new ReflectionClass($class); + + $parent_methods = []; + if ($parent = \get_parent_class($class)) { + $parent_methods = $this->getCachedMethods($parent); + } + + foreach ($r->getMethods() as $mr) { + if ($mr->isStatic()) { + continue; } - if ($method->owner_class !== $class && $d = $method->getRepresentation('method_definition')) { - $d = clone $d; - $d->inherited = true; - $method->replaceRepresentation($d); + $canon_name = \strtolower($mr->name); + if ($mr->isPrivate() && '__construct' !== $canon_name) { + $canon_name = \strtolower($mr->getDeclaringClass()->name).'::'.$canon_name; } - $rep->contents[] = $method; + if ($mr->getDeclaringClass()->name === $class) { + $method = new MethodValue(new MethodContext($mr), new DeclaredCallableBag($mr)); + $methods[$canon_name] = $method; + unset($parent_methods[$canon_name]); + } elseif (isset($parent_methods[$canon_name])) { + $method = $parent_methods[$canon_name]; + unset($parent_methods[$canon_name]); + + if (!$method->getContext()->inherited) { + $method = clone $method; + $method->getContext()->inherited = true; + } + + $methods[$canon_name] = $method; + } elseif ($mr->getDeclaringClass()->isInterface()) { + $c = new MethodContext($mr); + $c->inherited = true; + $methods[$canon_name] = new MethodValue($c, new DeclaredCallableBag($mr)); + } } - $o->addRepresentation($rep); + foreach ($parent_methods as $name => $method) { + if (!$method->getContext()->inherited) { + $method = clone $method; + $method->getContext()->inherited = true; + } + + if ('__construct' === $name) { + $methods['__construct'] = $method; + } else { + $methods[] = $method; + } + } + + $this->instance_cache[$class] = $methods; } + + return $this->instance_cache[$class]; } - private static function sort(MethodValue $a, MethodValue $b): int + /** + * @psalm-param class-string $class + * + * @psalm-return MethodValue[] + */ + private function getCachedStaticMethods(string $class): array { - $sort = ((int) $a->static) - ((int) $b->static); - if ($sort) { - return $sort; - } + if (!isset($this->static_cache[$class])) { + $methods = []; - $sort = Value::sortByAccess($a, $b); - if ($sort) { - return $sort; - } + $r = new ReflectionClass($class); + + $parent_methods = []; + if ($parent = \get_parent_class($class)) { + $parent_methods = $this->getCachedStaticMethods($parent); + } + + foreach ($r->getMethods(ReflectionMethod::IS_STATIC) as $mr) { + $canon_name = \strtolower($mr->getDeclaringClass()->name.'::'.$mr->name); + + if ($mr->getDeclaringClass()->name === $class) { + $method = new MethodValue(new MethodContext($mr), new DeclaredCallableBag($mr)); + $methods[$canon_name] = $method; + } elseif (isset($parent_methods[$canon_name])) { + $methods[$canon_name] = $parent_methods[$canon_name]; + } elseif ($mr->getDeclaringClass()->isInterface()) { + $c = new MethodContext($mr); + $c->inherited = true; + $methods[$canon_name] = new MethodValue($c, new DeclaredCallableBag($mr)); + } + + unset($parent_methods[$canon_name]); + } - $sort = InstanceValue::sortByHierarchy($a->owner_class, $b->owner_class); - if ($sort) { - return $sort; + $this->static_cache[$class] = $methods + $parent_methods; } - return $a->startline - $b->startline; + return $this->static_cache[$class]; } } diff --git a/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php b/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php index 5435da932bfc..17fa84ff3719 100644 --- a/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php +++ b/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php @@ -27,17 +27,23 @@ namespace Kint\Parser; -use Kint\Zval\InstanceValue; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\ClassConstContext; +use Kint\Value\Context\ClassDeclaredContext; +use Kint\Value\Context\ClassOwnedContext; +use Kint\Value\Context\StaticPropertyContext; +use Kint\Value\InstanceValue; +use Kint\Value\Representation\ContainerRepresentation; +use Kint\Value\UninitializedValue; use ReflectionClass; use ReflectionClassConstant; use ReflectionProperty; use UnitEnum; -class ClassStaticsPlugin extends AbstractPlugin +class ClassStaticsPlugin extends AbstractPlugin implements PluginCompleteInterface { - private static $cache = []; + /** @psalm-var array>> */ + private array $cache = []; public function getTypes(): array { @@ -49,106 +55,176 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + /** + * @psalm-template T of AbstractValue + * + * @psalm-param mixed $var + * @psalm-param T $v + * + * @psalm-return T + */ + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$o instanceof InstanceValue) { - return; + if (!$v instanceof InstanceValue) { + return $v; } - $class = \get_class($var); - $reflection = new ReflectionClass($class); + $class = $v->getClassName(); + $parser = $this->getParser(); + $r = new ReflectionClass($class); - // Constants - if (!isset(self::$cache[$class])) { - $consts = []; + $statics_full_name = false; + $statics = []; + $props = $r->getProperties(ReflectionProperty::IS_STATIC); + foreach ($props as $prop) { + $statics[$prop->name] = $prop; + } - foreach ($reflection->getConstants() as $name => $val) { - // Skip enum constants - if ($var instanceof UnitEnum && $val instanceof UnitEnum && $o->classname == \get_class($val)) { + $parent = $r; + while ($parent = $parent->getParentClass()) { + foreach ($parent->getProperties(ReflectionProperty::IS_STATIC) as $static) { + if (isset($statics[$static->name]) && $statics[$static->name]->getDeclaringClass()->name === $static->getDeclaringClass()->name) { continue; } - - $const = Value::blank($name); - $const->const = true; - $const->depth = $o->depth + 1; - $const->owner_class = $class; - $const->operator = Value::OPERATOR_STATIC; - - $creflection = new ReflectionClassConstant($class, $name); - - $const->access = Value::ACCESS_PUBLIC; - if ($creflection->isProtected()) { - $const->access = Value::ACCESS_PROTECTED; - } elseif ($creflection->isPrivate()) { - $const->access = Value::ACCESS_PRIVATE; - } - - if ($this->parser->childHasPath($o, $const)) { - $const->access_path = '\\'.$class.'::'.$name; - } - - $const = $this->parser->parse($val, $const); - - $consts[] = $const; + $statics[] = $static; } - - self::$cache[$class] = $consts; } - $statics = new Representation('Static class properties', 'statics'); - $statics->contents = self::$cache[$class]; + $statics_parsed = []; + $found_statics = []; + + $cdepth = $v->getContext()->getDepth(); - foreach ($reflection->getProperties(ReflectionProperty::IS_STATIC) as $static) { - $prop = new Value(); - $prop->name = '$'.$static->getName(); - $prop->depth = $o->depth + 1; - $prop->static = true; - $prop->operator = Value::OPERATOR_STATIC; - $prop->owner_class = $static->getDeclaringClass()->name; + foreach ($statics as $static) { + $prop = new StaticPropertyContext( + '$'.$static->getName(), + $static->getDeclaringClass()->name, + ClassDeclaredContext::ACCESS_PUBLIC + ); + $prop->depth = $cdepth + 1; + $prop->final = KINT_PHP84 && $static->isFinal(); - $prop->access = Value::ACCESS_PUBLIC; if ($static->isProtected()) { - $prop->access = Value::ACCESS_PROTECTED; + $prop->access = ClassDeclaredContext::ACCESS_PROTECTED; } elseif ($static->isPrivate()) { - $prop->access = Value::ACCESS_PRIVATE; + $prop->access = ClassDeclaredContext::ACCESS_PRIVATE; } - if ($this->parser->childHasPath($o, $prop)) { + if ($prop->isAccessible($parser->getCallerClass())) { $prop->access_path = '\\'.$prop->owner_class.'::'.$prop->name; } + if (isset($found_statics[$prop->name])) { + $statics_full_name = true; + } else { + $found_statics[$prop->name] = true; + + if ($prop->owner_class !== $class && ClassDeclaredContext::ACCESS_PRIVATE === $prop->access) { + $statics_full_name = true; + } + } + + if ($statics_full_name) { + $prop->name = $prop->owner_class.'::'.$prop->name; + } + $static->setAccessible(true); - if (KINT_PHP74 && !$static->isInitialized()) { - $prop->type = 'uninitialized'; - $statics->contents[] = $prop; + /** + * @psalm-suppress TooFewArguments + * Appears to have been fixed in master + */ + if (!$static->isInitialized()) { + $statics_parsed[] = new UninitializedValue($prop); } else { $static = $static->getValue(); - $statics->contents[] = $this->parser->parse($static, $prop); + $statics_parsed[] = $parser->parse($static, $prop); } } - if (empty($statics->contents)) { - return; + if ($statics_parsed) { + $v->addRepresentation(new ContainerRepresentation('Static properties', $statics_parsed, 'statics')); } - \usort($statics->contents, ['Kint\\Parser\\ClassStaticsPlugin', 'sort']); + if ($consts = $this->getCachedConstants($r)) { + $v->addRepresentation(new ContainerRepresentation('Class constants', $consts, 'constants')); + } - $o->addRepresentation($statics); + return $v; } - private static function sort(Value $a, Value $b): int + /** @psalm-return list */ + private function getCachedConstants(ReflectionClass $r): array { - $sort = ((int) $a->const) - ((int) $b->const); - if ($sort) { - return $sort; - } + $parser = $this->getParser(); + $pdepth = $parser->getDepthLimit(); + $pdepth_enabled = (int) ($pdepth > 0); + $class = $r->getName(); + + // Separate cache for dumping with/without depth limit + // This means we can do immediate depth limit on normal dumps + if (!isset($this->cache[$class][$pdepth_enabled])) { + $consts = []; + $reflectors = []; + + foreach ($r->getConstants() as $name => $val) { + $cr = new ReflectionClassConstant($class, $name); + + // Skip enum constants + if (\is_a($cr->class, UnitEnum::class, true) && $val instanceof UnitEnum && $cr->class === \get_class($val)) { + continue; + } + + $reflectors[$cr->name] = [$cr, $val]; + $consts[$cr->name] = null; + } + + if ($r = $r->getParentClass()) { + $parents = $this->getCachedConstants($r); + + foreach ($parents as $value) { + $c = $value->getContext(); + $cname = $c->getName(); + + if (isset($reflectors[$cname]) && $c instanceof ClassOwnedContext && $reflectors[$cname][0]->getDeclaringClass()->name === $c->owner_class) { + $consts[$cname] = $value; + unset($reflectors[$cname]); + } else { + $value = clone $value; + $c = $value->getContext(); + if ($c instanceof ClassOwnedContext) { + $c->name = $c->owner_class.'::'.$cname; + } + $consts[] = $value; + } + } + } + + foreach ($reflectors as [$cr, $val]) { + $context = new ClassConstContext( + $cr->name, + $cr->getDeclaringClass()->name, + ClassDeclaredContext::ACCESS_PUBLIC + ); + $context->depth = $pdepth ?: 1; + $context->final = KINT_PHP81 && $cr->isFinal(); + + if ($cr->isProtected()) { + $context->access = ClassDeclaredContext::ACCESS_PROTECTED; + } elseif ($cr->isPrivate()) { + $context->access = ClassDeclaredContext::ACCESS_PRIVATE; + } else { + // No access path for protected/private. Tough shit the cache is worth it + $context->access_path = '\\'.$context->owner_class.'::'.$context->name; + } + + $consts[$cr->name] = $parser->parse($val, $context); + } - $sort = Value::sortByAccess($a, $b); - if ($sort) { - return $sort; + /** @psalm-var AbstractValue[] $consts */ + $this->cache[$class][$pdepth_enabled] = \array_values($consts); } - return InstanceValue::sortByHierarchy($a->owner_class, $b->owner_class); + return $this->cache[$class][$pdepth_enabled]; } } diff --git a/system/ThirdParty/Kint/Parser/ClassStringsPlugin.php b/system/ThirdParty/Kint/Parser/ClassStringsPlugin.php new file mode 100644 index 000000000000..82927a655ce1 --- /dev/null +++ b/system/ThirdParty/Kint/Parser/ClassStringsPlugin.php @@ -0,0 +1,102 @@ +methods_plugin = new ClassMethodsPlugin($parser); + $this->statics_plugin = new ClassStaticsPlugin($parser); + } + + public function setParser(Parser $p): void + { + parent::setParser($p); + + $this->methods_plugin->setParser($p); + $this->statics_plugin->setParser($p); + } + + public function getTypes(): array + { + return ['string']; + } + + public function getTriggers(): int + { + return Parser::TRIGGER_SUCCESS; + } + + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue + { + $c = $v->getContext(); + + if ($c->getDepth() > 0) { + return $v; + } + + if (!\class_exists($var, true)) { + return $v; + } + + if (\in_array($var, self::$blacklist, true)) { + return $v; + } + + $r = new ReflectionClass($var); + + $fakeC = new BaseContext($c->getName()); + $fakeC->access_path = null; + $fakeV = new InstanceValue($fakeC, $r->getName(), 'badhash', -1); + $fakeVar = null; + + $fakeV = $this->methods_plugin->parseComplete($fakeVar, $fakeV, Parser::TRIGGER_SUCCESS); + $fakeV = $this->statics_plugin->parseComplete($fakeVar, $fakeV, Parser::TRIGGER_SUCCESS); + + foreach (['methods', 'static_methods', 'statics', 'constants'] as $rep) { + if ($rep = $fakeV->getRepresentation($rep)) { + $v->addRepresentation($rep); + } + } + + return $v; + } +} diff --git a/system/ThirdParty/Kint/Parser/ClosurePlugin.php b/system/ThirdParty/Kint/Parser/ClosurePlugin.php index 84ea582646f8..62db52547785 100644 --- a/system/ThirdParty/Kint/Parser/ClosurePlugin.php +++ b/system/ThirdParty/Kint/Parser/ClosurePlugin.php @@ -28,13 +28,14 @@ namespace Kint\Parser; use Closure; -use Kint\Zval\ClosureValue; -use Kint\Zval\ParameterValue; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\ClosureValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\Representation\ContainerRepresentation; use ReflectionFunction; +use ReflectionReference; -class ClosurePlugin extends AbstractPlugin +class ClosurePlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -46,29 +47,21 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if (!$var instanceof Closure) { - return; + return $v; } - $object = new ClosureValue(); - $object->transplant($o); - $o = $object; - $object->removeRepresentation('properties'); - - $closure = new ReflectionFunction($var); + $c = $v->getContext(); - $o->filename = $closure->getFileName(); - $o->startline = $closure->getStartLine(); + $object = new ClosureValue($c, $var); + $object->flags = $v->flags; + $object->appendRepresentations($v->getRepresentations()); - foreach ($closure->getParameters() as $param) { - $o->parameters[] = new ParameterValue($param); - } + $object->removeRepresentation('properties'); - $p = new Representation('Parameters'); - $p->contents = $o->parameters; - $o->addRepresentation($p, 0); + $closure = new ReflectionFunction($var); $statics = []; @@ -76,21 +69,25 @@ public function parse(&$var, Value &$o, int $trigger): void $statics = ['this' => $v]; } - if (\count($statics = $statics + $closure->getStaticVariables())) { + $statics = $statics + $closure->getStaticVariables(); + + $cdepth = $c->getDepth(); + + if (\count($statics)) { $statics_parsed = []; - foreach ($statics as $name => &$static) { - $obj = Value::blank('$'.$name); - $obj->depth = $o->depth + 1; - $statics_parsed[$name] = $this->parser->parse($static, $obj); - if (null === $statics_parsed[$name]->value) { - $statics_parsed[$name]->access_path = null; - } + $parser = $this->getParser(); + + foreach ($statics as $name => $_) { + $base = new BaseContext('$'.$name); + $base->depth = $cdepth + 1; + $base->reference = null !== ReflectionReference::fromArrayElement($statics, $name); + $statics_parsed[$name] = $parser->parse($statics[$name], $base); } - $r = new Representation('Uses'); - $r->contents = $statics_parsed; - $o->addRepresentation($r, 0); + $object->addRepresentation(new ContainerRepresentation('Uses', $statics_parsed), 0); } + + return $object; } } diff --git a/system/ThirdParty/Kint/Parser/ColorPlugin.php b/system/ThirdParty/Kint/Parser/ColorPlugin.php index 2a58cb9ad3b9..fc160a4342ee 100644 --- a/system/ThirdParty/Kint/Parser/ColorPlugin.php +++ b/system/ThirdParty/Kint/Parser/ColorPlugin.php @@ -27,10 +27,13 @@ namespace Kint\Parser; -use Kint\Zval\Representation\ColorRepresentation; -use Kint\Zval\Value; +use InvalidArgumentException; +use Kint\Value\AbstractValue; +use Kint\Value\ColorValue; +use Kint\Value\Representation\ColorRepresentation; +use Kint\Value\StringValue; -class ColorPlugin extends AbstractPlugin +class ColorPlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -42,24 +45,34 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if (\strlen($var) > 32) { - return; + return $v; + } + + if (!$v instanceof StringValue) { + return $v; } $trimmed = \strtolower(\trim($var)); if (!isset(ColorRepresentation::$color_map[$trimmed]) && !\preg_match('/^(?:(?:rgb|hsl)[^\\)]{6,}\\)|#[0-9a-fA-F]{3,8})$/', $trimmed)) { - return; + return $v; } - $rep = new ColorRepresentation($var); - - if ($rep->variant) { - $o->removeRepresentation($o->value); - $o->addRepresentation($rep, 0); - $o->hints[] = 'color'; + try { + $rep = new ColorRepresentation($var); + } catch (InvalidArgumentException $e) { + return $v; } + + $out = new ColorValue($v->getContext(), $v->getValue(), $v->getEncoding()); + $out->flags = $v->flags; + $out->appendRepresentations($v->getRepresentations()); + $out->removeRepresentation('contents'); + $out->addRepresentation($rep, 0); + + return $out; } } diff --git a/system/ThirdParty/Kint/Parser/ConstructablePluginInterface.php b/system/ThirdParty/Kint/Parser/ConstructablePluginInterface.php index 689b24e61668..880b57efb72f 100644 --- a/system/ThirdParty/Kint/Parser/ConstructablePluginInterface.php +++ b/system/ThirdParty/Kint/Parser/ConstructablePluginInterface.php @@ -29,5 +29,5 @@ interface ConstructablePluginInterface extends PluginInterface { - public function __construct(); + public function __construct(Parser $p); } diff --git a/system/ThirdParty/Kint/Parser/DOMDocumentPlugin.php b/system/ThirdParty/Kint/Parser/DOMDocumentPlugin.php deleted file mode 100644 index 84cfbf835f26..000000000000 --- a/system/ThirdParty/Kint/Parser/DOMDocumentPlugin.php +++ /dev/null @@ -1,356 +0,0 @@ - 'DOMNode', - 'firstChild' => 'DOMNode', - 'lastChild' => 'DOMNode', - 'previousSibling' => 'DOMNode', - 'nextSibling' => 'DOMNode', - 'ownerDocument' => 'DOMDocument', - ]; - - /** - * Show all properties and methods. - * - * @var bool - */ - public static $verbose = false; - - public function getTypes(): array - { - return ['object']; - } - - public function getTriggers(): int - { - return Parser::TRIGGER_SUCCESS; - } - - public function parse(&$var, Value &$o, int $trigger): void - { - if (!$o instanceof InstanceValue) { - return; - } - - if ($var instanceof DOMNamedNodeMap || $var instanceof DOMNodeList) { - $this->parseList($var, $o, $trigger); - - return; - } - - if ($var instanceof DOMNode) { - $this->parseNode($var, $o); - - return; - } - } - - /** - * @param DOMNamedNodeMap|DOMNodeList &$var - */ - protected function parseList($var, InstanceValue &$o, int $trigger): void - { - if (!$var instanceof DOMNamedNodeMap && !$var instanceof DOMNodeList) { - return; - } - - // Recursion should never happen, should always be stopped at the parent - // DOMNode. Depth limit on the other hand we're going to skip since - // that would show an empty iterator and rather useless. Let the depth - // limit hit the children (DOMNodeList only has DOMNode as children) - if ($trigger & Parser::TRIGGER_RECURSION) { - return; - } - - $o->size = $var->length; - if (0 === $o->size) { - $o->replaceRepresentation(new Representation('Iterator')); - $o->size = null; - - return; - } - - // Depth limit - // Make empty iterator representation since we need it in DOMNode to point out depth limits - if ($this->parser->getDepthLimit() && $o->depth + 1 >= $this->parser->getDepthLimit()) { - $b = new Value(); - $b->name = $o->classname.' Iterator Contents'; - $b->access_path = 'iterator_to_array('.$o->access_path.')'; - $b->depth = $o->depth + 1; - $b->hints[] = 'depth_limit'; - - $r = new Representation('Iterator'); - $r->contents = [$b]; - $o->replaceRepresentation($r, 0); - - return; - } - - $r = new Representation('Iterator'); - $o->replaceRepresentation($r, 0); - - foreach ($var as $key => $item) { - $base_obj = new Value(); - $base_obj->depth = $o->depth + 1; - $base_obj->name = $item->nodeName; - - if ($o->access_path) { - if ($var instanceof DOMNamedNodeMap) { - // We can't use getNamedItem() for attributes without a - // namespace because it will pick the first matching - // attribute of *any* namespace. - // - // Contrary to the PHP docs, getNamedItemNS takes null - // as a namespace argument for an unnamespaced item. - $base_obj->access_path = $o->access_path.'->getNamedItemNS('; - $base_obj->access_path .= \var_export($item->namespaceURI, true); - $base_obj->access_path .= ', '; - $base_obj->access_path .= \var_export($item->name, true); - $base_obj->access_path .= ')'; - } else { // DOMNodeList - $base_obj->access_path = $o->access_path.'->item('.\var_export($key, true).')'; - } - } - - $r->contents[] = $this->parser->parse($item, $base_obj); - } - } - - /** - * @psalm-param-out Value &$o - */ - protected function parseNode(DOMNode $var, InstanceValue &$o): void - { - // Fill the properties - // They can't be enumerated through reflection or casting, - // so we have to trust the docs and try them one at a time - $known_properties = [ - 'nodeValue', - 'childNodes', - 'attributes', - ]; - - if (self::$verbose) { - $known_properties = [ - 'nodeName', - 'nodeValue', - 'nodeType', - 'parentNode', - 'childNodes', - 'firstChild', - 'lastChild', - 'previousSibling', - 'nextSibling', - 'attributes', - 'ownerDocument', - 'namespaceURI', - 'prefix', - 'localName', - 'baseURI', - 'textContent', - ]; - } - - $childNodes = null; - $attributes = null; - - $rep = $o->value; - - foreach ($known_properties as $prop) { - $prop_obj = $this->parseProperty($o, $prop, $var); - $rep->contents[] = $prop_obj; - - if ('childNodes' === $prop) { - $childNodes = $prop_obj->getRepresentation('iterator'); - } elseif ('attributes' === $prop) { - $attributes = $prop_obj->getRepresentation('iterator'); - } - } - - if (!self::$verbose) { - $o->removeRepresentation('methods'); - $o->removeRepresentation('properties'); - } - - // Attributes and comments and text nodes don't - // need children or attributes of their own - if (\in_array($o->classname, ['DOMAttr', 'DOMText', 'DOMComment'], true)) { - $o = self::textualNodeToString($o); - - return; - } - - // Set the attributes - if ($attributes) { - $a = new Representation('Attributes'); - foreach ($attributes->contents as $attribute) { - $a->contents[] = $attribute; - } - $o->addRepresentation($a, 0); - } - - // Set the children - if ($childNodes) { - $c = new Representation('Children'); - - if (1 === \count($childNodes->contents) && ($node = \reset($childNodes->contents)) && \in_array('depth_limit', $node->hints, true)) { - $n = new InstanceValue(); - $n->transplant($node); - $n->name = 'childNodes'; - $n->classname = 'DOMNodeList'; - $c->contents = [$n]; - } else { - foreach ($childNodes->contents as $node) { - // Remove text nodes if theyre empty - if ($node instanceof BlobValue && '#text' === $node->name && (\ctype_space($node->value->contents) || '' === $node->value->contents)) { - continue; - } - - $c->contents[] = $node; - } - } - - $o->addRepresentation($c, 0); - } - - if ($childNodes) { - $o->size = \count($childNodes->contents); - } - - if (!$o->size) { - $o->size = null; - } - } - - protected function parseProperty(InstanceValue $o, string $prop, DOMNode &$var): Value - { - // Duplicating (And slightly optimizing) the Parser::parseObject() code here - $base_obj = new Value(); - $base_obj->depth = $o->depth + 1; - $base_obj->owner_class = $o->classname; - $base_obj->name = $prop; - $base_obj->operator = Value::OPERATOR_OBJECT; - $base_obj->access = Value::ACCESS_PUBLIC; - - if (null !== $o->access_path) { - $base_obj->access_path = $o->access_path; - - if (\preg_match('/^[A-Za-z0-9_]+$/', $base_obj->name)) { - $base_obj->access_path .= '->'.$base_obj->name; - } else { - $base_obj->access_path .= '->{'.\var_export($base_obj->name, true).'}'; - } - } - - if (!isset($var->{$prop})) { - $base_obj->type = 'null'; - } elseif (isset(self::$blacklist[$prop])) { - $b = new InstanceValue(); - $b->transplant($base_obj); - $base_obj = $b; - - $base_obj->hints[] = 'blacklist'; - $base_obj->classname = self::$blacklist[$prop]; - } elseif ('attributes' === $prop) { - // Attributes are strings. If we're too deep set the - // depth limit to enable parsing them, but no deeper. - if ($this->parser->getDepthLimit() && $this->parser->getDepthLimit() - 2 < $base_obj->depth) { - $base_obj->depth = $this->parser->getDepthLimit() - 2; - } - $base_obj = $this->parser->parse($var->{$prop}, $base_obj); - } else { - $base_obj = $this->parser->parse($var->{$prop}, $base_obj); - } - - return $base_obj; - } - - protected static function textualNodeToString(InstanceValue $o): Value - { - if (empty($o->value) || empty($o->value->contents) || empty($o->classname)) { - throw new InvalidArgumentException('Invalid DOMNode passed to DOMDocumentPlugin::textualNodeToString'); - } - - if (!\in_array($o->classname, ['DOMText', 'DOMAttr', 'DOMComment'], true)) { - throw new InvalidArgumentException('Invalid DOMNode passed to DOMDocumentPlugin::textualNodeToString'); - } - - foreach ($o->value->contents as $property) { - if ('nodeValue' === $property->name) { - $ret = clone $property; - $ret->name = $o->name; - - return $ret; - } - } - - throw new InvalidArgumentException('Invalid DOMNode passed to DOMDocumentPlugin::textualNodeToString'); - } -} diff --git a/system/ThirdParty/Kint/Parser/DateTimePlugin.php b/system/ThirdParty/Kint/Parser/DateTimePlugin.php index 038acea133ec..4cba25d48dc9 100644 --- a/system/ThirdParty/Kint/Parser/DateTimePlugin.php +++ b/system/ThirdParty/Kint/Parser/DateTimePlugin.php @@ -27,11 +27,13 @@ namespace Kint\Parser; -use DateTime; -use Kint\Zval\DateTimeValue; -use Kint\Zval\Value; +use DateTimeInterface; +use Error; +use Kint\Value\AbstractValue; +use Kint\Value\DateTimeValue; +use Kint\Value\InstanceValue; -class DateTimePlugin extends AbstractPlugin +class DateTimePlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -43,15 +45,23 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$var instanceof DateTime) { - return; + if (!$var instanceof DateTimeInterface || !$v instanceof InstanceValue) { + return $v; } - $object = new DateTimeValue($var); - $object->transplant($o); + try { + $dtv = new DateTimeValue($v->getContext(), $var); + } catch (Error $e) { + // Only happens if someone makes a DateTimeInterface with a private __clone + return $v; + } + + $dtv->setChildren($v->getChildren()); + $dtv->flags = $v->flags; + $dtv->appendRepresentations($v->getRepresentations()); - $o = $object; + return $dtv; } } diff --git a/system/ThirdParty/Kint/Parser/DomPlugin.php b/system/ThirdParty/Kint/Parser/DomPlugin.php new file mode 100644 index 000000000000..d12e2f4cc8d9 --- /dev/null +++ b/system/ThirdParty/Kint/Parser/DomPlugin.php @@ -0,0 +1,528 @@ + Property names to readable status + */ + public const NODE_PROPS = [ + 'nodeType' => true, + 'nodeName' => true, + 'baseURI' => true, + 'isConnected' => true, + 'ownerDocument' => true, + 'parentNode' => true, + 'parentElement' => true, + 'childNodes' => true, + 'firstChild' => true, + 'lastChild' => true, + 'previousSibling' => true, + 'nextSibling' => true, + 'nodeValue' => true, + 'textContent' => false, + ]; + + /** + * @psalm-var non-empty-array Property names to readable status + */ + public const ELEMENT_PROPS = [ + 'namespaceURI' => true, + 'prefix' => true, + 'localName' => true, + 'tagName' => true, + 'id' => false, + 'className' => false, + 'classList' => true, + 'attributes' => true, + 'firstElementChild' => true, + 'lastElementChild' => true, + 'childElementCount' => true, + 'previousElementSibling' => true, + 'nextElementSibling' => true, + 'innerHTML' => false, + 'substitutedNodeValue' => false, + ]; + + /** + * @psalm-var non-empty-array Property names to readable status + */ + public const DOMNODE_PROPS = [ + 'nodeName' => true, + 'nodeValue' => false, + 'nodeType' => true, + 'parentNode' => true, + 'parentElement' => true, + 'childNodes' => true, + 'firstChild' => true, + 'lastChild' => true, + 'previousSibling' => true, + 'nextSibling' => true, + 'attributes' => true, + 'isConnected' => true, + 'ownerDocument' => true, + 'namespaceURI' => true, + 'prefix' => false, + 'localName' => true, + 'baseURI' => true, + 'textContent' => false, + ]; + + /** + * @psalm-var non-empty-array Property names to readable status + */ + public const DOMELEMENT_PROPS = [ + 'tagName' => true, + 'className' => false, + 'id' => false, + 'schemaTypeInfo' => true, + 'firstElementChild' => true, + 'lastElementChild' => true, + 'childElementCount' => true, + 'previousElementSibling' => true, + 'nextElementSibling' => true, + ]; + + public const DOM_VERSIONS = [ + 'parentElement' => KINT_PHP83, + 'isConnected' => KINT_PHP83, + 'className' => KINT_PHP83, + 'id' => KINT_PHP83, + 'firstElementChild' => KINT_PHP80, + 'lastElementChild' => KINT_PHP80, + 'childElementCount' => KINT_PHP80, + 'previousElementSibling' => KINT_PHP80, + 'nextElementSibling' => KINT_PHP80, + ]; + + /** + * List of properties to skip parsing. + * + * The properties of a Dom\Node can do a *lot* of damage to debuggers. The + * Dom\Node contains not one, not two, but 13 different ways to recurse into itself: + * * parentNode + * * firstChild + * * lastChild + * * previousSibling + * * nextSibling + * * parentElement + * * firstElementChild + * * lastElementChild + * * previousElementSibling + * * nextElementSibling + * * childNodes + * * attributes + * * ownerDocument + * + * All of this combined: the tiny SVGs used as the caret in Kint were already + * enough to make parsing and rendering take over a second, and send memory + * usage over 128 megs, back in the old DOM API. So we blacklist every field + * we don't strictly need and hope that that's good enough. + * + * In retrospect -- this is probably why print_r does the same + * + * @psalm-var array + */ + public static array $blacklist = [ + 'parentNode' => true, + 'firstChild' => true, + 'lastChild' => true, + 'previousSibling' => true, + 'nextSibling' => true, + 'firstElementChild' => true, + 'lastElementChild' => true, + 'parentElement' => true, + 'previousElementSibling' => true, + 'nextElementSibling' => true, + 'ownerDocument' => true, + ]; + + /** + * Show all properties and methods. + */ + public static bool $verbose = false; + + protected ClassMethodsPlugin $methods_plugin; + protected ClassStaticsPlugin $statics_plugin; + + public function __construct(Parser $parser) + { + parent::__construct($parser); + + $this->methods_plugin = new ClassMethodsPlugin($parser); + $this->statics_plugin = new ClassStaticsPlugin($parser); + } + + public function setParser(Parser $p): void + { + parent::setParser($p); + + $this->methods_plugin->setParser($p); + $this->statics_plugin->setParser($p); + } + + public function getTypes(): array + { + return ['object']; + } + + public function getTriggers(): int + { + return Parser::TRIGGER_BEGIN; + } + + public function parseBegin(&$var, ContextInterface $c): ?AbstractValue + { + // Attributes and chardata (Which is parent of comments and text + // nodes) don't need children or attributes of their own + if ($var instanceof Attr || $var instanceof CharacterData || $var instanceof DOMAttr || $var instanceof DOMCharacterData) { + return $this->parseText($var, $c); + } + + if ($var instanceof NamedNodeMap || $var instanceof NodeList || $var instanceof DOMNamedNodeMap || $var instanceof DOMNodeList) { + return $this->parseList($var, $c); + } + + if ($var instanceof Node || $var instanceof DOMNode) { + return $this->parseNode($var, $c); + } + + return null; + } + + /** @psalm-param Node|DOMNode $var */ + private function parseProperty(object $var, string $prop, ContextInterface $c): AbstractValue + { + if (!isset($var->{$prop})) { + return new FixedWidthValue($c, null); + } + + $parser = $this->getParser(); + $value = $var->{$prop}; + + if (\is_scalar($value)) { + return $parser->parse($value, $c); + } + + if (isset(self::$blacklist[$prop])) { + $b = new InstanceValue($c, \get_class($value), \spl_object_hash($value), \spl_object_id($value)); + $b->flags |= AbstractValue::FLAG_GENERATED | AbstractValue::FLAG_BLACKLIST; + + return $b; + } + + // Everything we can handle in parseBegin + if ($value instanceof Attr || $value instanceof CharacterData || $value instanceof DOMAttr || $value instanceof DOMCharacterData || $value instanceof NamedNodeMap || $value instanceof NodeList || $value instanceof DOMNamedNodeMap || $value instanceof DOMNodeList || $value instanceof Node || $value instanceof DOMNode) { + $out = $this->parseBegin($value, $c); + } + + if (!isset($out)) { + // Shouldn't ever happen + $out = $parser->parse($value, $c); // @codeCoverageIgnore + } + + $out->flags |= AbstractValue::FLAG_GENERATED; + + return $out; + } + + /** @psalm-param Attr|CharacterData|DOMAttr|DOMCharacterData $var */ + private function parseText(object $var, ContextInterface $c): AbstractValue + { + if ($c instanceof BaseContext && null !== $c->access_path) { + $c->access_path .= '->nodeValue'; + } + + return $this->parseProperty($var, 'nodeValue', $c); + } + + /** @psalm-param NamedNodeMap|NodeList|DOMNamedNodeMap|DOMNodeList $var */ + private function parseList(object $var, ContextInterface $c): InstanceValue + { + if ($var instanceof NodeList || $var instanceof DOMNodeList) { + $v = new DomNodeListValue($c, $var); + } else { + $v = new InstanceValue($c, \get_class($var), \spl_object_hash($var), \spl_object_id($var)); + } + + $parser = $this->getParser(); + $pdepth = $parser->getDepthLimit(); + + // Depth limit + // Use empty iterator representation since we need it to point out depth limits + if (($var instanceof NodeList || $var instanceof DOMNodeList) && $pdepth && $c->getDepth() >= $pdepth) { + $v->flags |= AbstractValue::FLAG_DEPTH_LIMIT; + + return $v; + } + + if (self::$verbose) { + $v = $this->methods_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS); + $v = $this->statics_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS); + } + + if (0 === $var->length) { + $v->setChildren([]); + + return $v; + } + + $cdepth = $c->getDepth(); + $ap = $c->getAccessPath(); + $contents = []; + + foreach ($var as $key => $item) { + $base_obj = new BaseContext($item->nodeName); + $base_obj->depth = $cdepth + 1; + + if ($var instanceof NamedNodeMap || $var instanceof DOMNamedNodeMap) { + if (null !== $ap) { + $base_obj->access_path = $ap.'['.\var_export($item->nodeName, true).']'; + } + } else { // NodeList + if (null !== $ap) { + $base_obj->access_path = $ap.'['.\var_export($key, true).']'; + } + } + + if ($item instanceof HTMLElement) { + $base_obj->name = $item->localName; + } + + $item = $parser->parse($item, $base_obj); + $item->flags |= AbstractValue::FLAG_GENERATED; + + $contents[] = $item; + } + + $v->setChildren($contents); + + if ($contents) { + $v->addRepresentation(new ContainerRepresentation('Iterator', $contents), 0); + } + + return $v; + } + + /** @psalm-param Node|DOMNode $var */ + private function parseNode(object $var, ContextInterface $c): DomNodeValue + { + $class = \get_class($var); + $pdepth = $this->getParser()->getDepthLimit(); + + if ($pdepth && $c->getDepth() >= $pdepth) { + $v = new DomNodeValue($c, $var); + $v->flags |= AbstractValue::FLAG_DEPTH_LIMIT; + + return $v; + } + + if (($var instanceof DocumentType || $var instanceof DOMDocumentType) && $c instanceof BaseContext && $c->name === $var->nodeName) { + $c->name = '!DOCTYPE '.$c->name; + } + + $cdepth = $c->getDepth(); + $ap = $c->getAccessPath(); + + $properties = []; + $children = []; + $attributes = []; + + foreach (self::getKnownProperties($var) as $prop => $readonly) { + $prop_c = new PropertyContext($prop, $class, ClassDeclaredContext::ACCESS_PUBLIC); + $prop_c->depth = $cdepth + 1; + $prop_c->readonly = KINT_PHP81 && $readonly; + + if (null !== $ap) { + $prop_c->access_path = $ap.'->'.$prop; + } + + $properties[] = $prop_obj = $this->parseProperty($var, $prop, $prop_c); + + if ('childNodes' === $prop) { + if (!$prop_obj instanceof DomNodeListValue) { + throw new LogicException('childNodes property parsed incorrectly'); // @codeCoverageIgnore + } + $children = self::getChildren($prop_obj); + } elseif ('attributes' === $prop) { + $attributes = $prop_obj->getRepresentation('iterator'); + $attributes = $attributes instanceof ContainerRepresentation ? $attributes->getContents() : []; + } elseif ('classList' === $prop) { + if ($iter = $prop_obj->getRepresentation('iterator')) { + $prop_obj->removeRepresentation($iter); + $prop_obj->addRepresentation($iter, 0); + } + } + } + + $v = new DomNodeValue($c, $var); + // If we're in text mode, we can see children through the childNodes property + $v->setChildren($properties); + + if ($children) { + $v->addRepresentation(new ContainerRepresentation('Children', $children, null, true)); + } + + if ($attributes) { + $v->addRepresentation(new ContainerRepresentation('Attributes', $attributes)); + } + + if (self::$verbose) { + $v->addRepresentation(new ContainerRepresentation('Properties', $properties)); + + $v = $this->methods_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS); + $v = $this->statics_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS); + } + + return $v; + } + + /** + * @psalm-param Node|DOMNode $var + * + * @psalm-return non-empty-array + */ + public static function getKnownProperties(object $var): array + { + if ($var instanceof Node) { + $known_properties = self::NODE_PROPS; + if ($var instanceof Element) { + $known_properties += self::ELEMENT_PROPS; + } + + if ($var instanceof Document) { + $known_properties['textContent'] = true; + } + + if ($var instanceof Attr || $var instanceof CharacterData) { + $known_properties['nodeValue'] = false; + } + } else { + $known_properties = self::DOMNODE_PROPS; + if ($var instanceof DOMElement) { + $known_properties += self::DOMELEMENT_PROPS; + } + + foreach (self::DOM_VERSIONS as $key => $val) { + /** + * @psalm-var bool $val + * Psalm bug #4509 + */ + if (false === $val) { + unset($known_properties[$key]); // @codeCoverageIgnore + } + } + } + + /** @psalm-var non-empty-array $known_properties */ + if (!self::$verbose) { + $known_properties = \array_intersect_key($known_properties, [ + 'nodeValue' => null, + 'childNodes' => null, + 'attributes' => null, + ]); + } + + return $known_properties; + } + + /** @psalm-return list */ + private static function getChildren(DomNodeListValue $property): array + { + if (0 === $property->getLength()) { + return []; + } + + if ($property->flags & AbstractValue::FLAG_DEPTH_LIMIT) { + return [$property]; + } + + $list_items = $property->getChildren(); + + if (null === $list_items) { + // This is here for psalm but all DomNodeListValue should + // either be depth_limit or have array children + return []; // @codeCoverageIgnore + } + + $children = []; + + foreach ($list_items as $node) { + // Remove text nodes if theyre empty + if ($node instanceof StringValue && '#text' === $node->getContext()->getName()) { + /** + * @psalm-suppress InvalidArgument + * Psalm bug #11055 + */ + if (\ctype_space($node->getValue()) || '' === $node->getValue()) { + continue; + } + } + + $children[] = $node; + } + + return $children; + } +} diff --git a/system/ThirdParty/Kint/Parser/EnumPlugin.php b/system/ThirdParty/Kint/Parser/EnumPlugin.php index d5d348aa5c90..e1fe85b65201 100644 --- a/system/ThirdParty/Kint/Parser/EnumPlugin.php +++ b/system/ThirdParty/Kint/Parser/EnumPlugin.php @@ -27,15 +27,15 @@ namespace Kint\Parser; -use BackedEnum; -use Kint\Zval\EnumValue; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\EnumValue; +use Kint\Value\Representation\ContainerRepresentation; use UnitEnum; -class EnumPlugin extends AbstractPlugin +class EnumPlugin extends AbstractPlugin implements PluginCompleteInterface { - private static $cache = []; + private array $cache = []; public function getTypes(): array { @@ -51,38 +51,34 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if (!$var instanceof UnitEnum) { - return; + return $v; } + $c = $v->getContext(); $class = \get_class($var); - if (!isset(self::$cache[$class])) { - $cases = new Representation('Enum values', 'enum'); - $cases->contents = []; + if (!isset($this->cache[$class])) { + $contents = []; foreach ($var->cases() as $case) { - $base_obj = Value::blank($class.'::'.$case->name, '\\'.$class.'::'.$case->name); - $base_obj->depth = $o->depth + 1; - - if ($var instanceof BackedEnum) { - $c = $case->value; - $cases->contents[] = $this->parser->parse($c, $base_obj); - } else { - $cases->contents[] = $base_obj; - } + $base = new BaseContext($case->name); + $base->access_path = '\\'.$class.'::'.$case->name; + $base->depth = $c->getDepth() + 1; + $contents[] = new EnumValue($base, $case); } - self::$cache[$class] = $cases; + /** @psalm-var non-empty-array $contents */ + $this->cache[$class] = new ContainerRepresentation('Enum values', $contents, 'enum'); } - $object = new EnumValue($var); - $object->transplant($o); - - $object->addRepresentation(self::$cache[$class], 0); + $object = new EnumValue($c, $var); + $object->flags = $v->flags; + $object->appendRepresentations($v->getRepresentations()); + $object->addRepresentation($this->cache[$class], 0); - $o = $object; + return $object; } } diff --git a/system/ThirdParty/Kint/Parser/FsPathPlugin.php b/system/ThirdParty/Kint/Parser/FsPathPlugin.php index 1a98c6dcd134..3b17d0e91142 100644 --- a/system/ThirdParty/Kint/Parser/FsPathPlugin.php +++ b/system/ThirdParty/Kint/Parser/FsPathPlugin.php @@ -27,14 +27,14 @@ namespace Kint\Parser; -use Kint\Zval\Representation\SplFileInfoRepresentation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Representation\SplFileInfoRepresentation; use SplFileInfo; use TypeError; -class FsPathPlugin extends AbstractPlugin +class FsPathPlugin extends AbstractPlugin implements PluginCompleteInterface { - public static $blacklist = ['/', '.']; + public static array $blacklist = ['/', '.']; public function getTypes(): array { @@ -46,35 +46,35 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if (\strlen($var) > 2048) { - return; + return $v; } if (!\preg_match('/[\\/\\'.DIRECTORY_SEPARATOR.']/', $var)) { - return; + return $v; } if (\preg_match('/[?<>"*|]/', $var)) { - return; + return $v; } try { if (!@\file_exists($var)) { - return; + return $v; } } catch (TypeError $e) {// @codeCoverageIgnore // Only possible in PHP 7 - return; // @codeCoverageIgnore + return $v; // @codeCoverageIgnore } if (\in_array($var, self::$blacklist, true)) { - return; + return $v; } - $r = new SplFileInfoRepresentation(new SplFileInfo($var)); - $r->hints[] = 'fspath'; - $o->addRepresentation($r, 0); + $v->addRepresentation(new SplFileInfoRepresentation(new SplFileInfo($var)), 0); + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/HtmlPlugin.php b/system/ThirdParty/Kint/Parser/HtmlPlugin.php new file mode 100644 index 000000000000..4df7ad1993fd --- /dev/null +++ b/system/ThirdParty/Kint/Parser/HtmlPlugin.php @@ -0,0 +1,86 @@ +' !== \strtolower(\substr($var, 0, 15))) { + return $v; + } + + try { + $html = HTMLDocument::createFromString($var, LIBXML_NOERROR); + } catch (DOMException $e) { // @codeCoverageIgnore + return $v; // @codeCoverageIgnore + } + + $c = $v->getContext(); + + $base = new BaseContext('childNodes'); + $base->depth = $c->getDepth(); + + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = '\\Dom\\HTMLDocument::createFromString('.$ap.')->childNodes'; + } + + $out = $this->getParser()->parse($html->childNodes, $base); + $iter = $out->getRepresentation('iterator'); + + if ($out->flags & AbstractValue::FLAG_DEPTH_LIMIT) { + $out->flags |= AbstractValue::FLAG_GENERATED; + $v->addRepresentation(new ValueRepresentation('HTML', $out), 0); + } elseif ($iter instanceof ContainerRepresentation) { + $v->addRepresentation(new ContainerRepresentation('HTML', $iter->getContents()), 0); + } + + return $v; + } +} diff --git a/system/ThirdParty/Kint/Parser/IteratorPlugin.php b/system/ThirdParty/Kint/Parser/IteratorPlugin.php index 7ebfe73e5cef..fff51ee491d7 100644 --- a/system/ThirdParty/Kint/Parser/IteratorPlugin.php +++ b/system/ThirdParty/Kint/Parser/IteratorPlugin.php @@ -27,11 +27,25 @@ namespace Kint\Parser; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Dom\NamedNodeMap; +use Dom\NodeList; +use DOMNamedNodeMap; +use DOMNodeList; +use Kint\Value\AbstractValue; +use Kint\Value\ArrayValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\InstanceValue; +use Kint\Value\Representation\ContainerRepresentation; +use Kint\Value\Representation\ValueRepresentation; +use Kint\Value\UninitializedValue; +use mysqli_result; +use PDOStatement; +use SimpleXMLElement; +use SplFileObject; +use Throwable; use Traversable; -class IteratorPlugin extends AbstractPlugin +class IteratorPlugin extends AbstractPlugin implements PluginCompleteInterface { /** * List of classes and interfaces to blacklist. @@ -40,14 +54,17 @@ class IteratorPlugin extends AbstractPlugin * when traversed. Others are just huge. Either way, put them in here * and you won't have to worry about them being parsed. * - * @var array + * @psalm-var class-string[] */ - public static $blacklist = [ - 'DOMNamedNodeMap', - 'DOMNodeList', - 'mysqli_result', - 'PDOStatement', - 'SplFileObject', + public static array $blacklist = [ + NamedNodeMap::class, + NodeList::class, + DOMNamedNodeMap::class, + DOMNodeList::class, + mysqli_result::class, + PDOStatement::class, + SimpleXMLElement::class, + SplFileObject::class, ]; public function getTypes(): array @@ -60,48 +77,70 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$var instanceof Traversable) { - return; + if (!$var instanceof Traversable || !$v instanceof InstanceValue || $v->getRepresentation('iterator')) { + return $v; } + $c = $v->getContext(); + foreach (self::$blacklist as $class) { + /** + * @psalm-suppress RedundantCondition + * Psalm bug #11076 + */ if ($var instanceof $class) { - $b = new Value(); - $b->name = $class.' Iterator Contents'; - $b->access_path = 'iterator_to_array('.$o->access_path.', true)'; - $b->depth = $o->depth + 1; - $b->hints[] = 'blacklist'; + $base = new BaseContext($class.' Iterator Contents'); + $base->depth = $c->getDepth() + 1; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = 'iterator_to_array('.$ap.', false)'; + } - $r = new Representation('Iterator'); - $r->contents = [$b]; + $b = new UninitializedValue($base); + $b->flags |= AbstractValue::FLAG_BLACKLIST; - $o->addRepresentation($r); + $v->addRepresentation(new ValueRepresentation('Iterator', $b)); - return; + return $v; } } - $data = \iterator_to_array($var); + try { + $data = \iterator_to_array($var, false); + } catch (Throwable $t) { + return $v; + } - $base_obj = new Value(); - $base_obj->depth = $o->depth; + if (!\count($data)) { + return $v; + } - if ($o->access_path) { - $base_obj->access_path = 'iterator_to_array('.$o->access_path.')'; + $base = new BaseContext('Iterator Contents'); + $base->depth = $c->getDepth(); + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = 'iterator_to_array('.$ap.', false)'; } - $r = new Representation('Iterator'); - $r->contents = $this->parser->parse($data, $base_obj); - $r->contents = $r->contents->value->contents; + $iter_val = $this->getParser()->parse($data, $base); - $primary = $o->getRepresentations(); - $primary = \reset($primary); - if ($primary && $primary === $o->value && [] === $primary->contents) { - $o->addRepresentation($r, 0); + // Since we didn't get TRIGGER_DEPTH_LIMIT and set the iterator to the + // same depth we can assume at least 1 level deep will exist + if ($iter_val instanceof ArrayValue && $iterator_items = $iter_val->getContents()) { + $r = new ContainerRepresentation('Iterator', $iterator_items); + $iterator_items = \array_values($iterator_items); } else { - $o->addRepresentation($r); + $r = new ValueRepresentation('Iterator', $iter_val); + $iterator_items = [$iter_val]; } + + if ((bool) $v->getChildren()) { + $v->addRepresentation($r); + } else { + $v->setChildren($iterator_items); + $v->addRepresentation($r, 0); + } + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/JsonPlugin.php b/system/ThirdParty/Kint/Parser/JsonPlugin.php index 6bcf3a6130ce..cefdb11bda8f 100644 --- a/system/ThirdParty/Kint/Parser/JsonPlugin.php +++ b/system/ThirdParty/Kint/Parser/JsonPlugin.php @@ -27,10 +27,14 @@ namespace Kint\Parser; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use JsonException; +use Kint\Value\AbstractValue; +use Kint\Value\ArrayValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\Representation\ContainerRepresentation; +use Kint\Value\Representation\ValueRepresentation; -class JsonPlugin extends AbstractPlugin +class JsonPlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -42,34 +46,41 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if (!isset($var[0]) || ('{' !== $var[0] && '[' !== $var[0])) { - return; + return $v; } - $json = \json_decode($var, true); - - if (!$json) { - return; + try { + $json = \json_decode($var, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + return $v; } $json = (array) $json; - $base_obj = new Value(); - $base_obj->depth = $o->depth; + $c = $v->getContext(); + + $base = new BaseContext('JSON Decode'); + $base->depth = $c->getDepth(); - if ($o->access_path) { - $base_obj->access_path = 'json_decode('.$o->access_path.', true)'; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = 'json_decode('.$ap.', true)'; } - $r = new Representation('Json'); - $r->contents = $this->parser->parse($json, $base_obj); + $json = $this->getParser()->parse($json, $base); - if (!\in_array('depth_limit', $r->contents->hints, true)) { - $r->contents = $r->contents->value->contents; + if ($json instanceof ArrayValue && (~$json->flags & AbstractValue::FLAG_DEPTH_LIMIT) && $contents = $json->getContents()) { + foreach ($contents as $value) { + $value->flags |= AbstractValue::FLAG_GENERATED; + } + $v->addRepresentation(new ContainerRepresentation('Json', $contents), 0); + } else { + $json->flags |= AbstractValue::FLAG_GENERATED; + $v->addRepresentation(new ValueRepresentation('Json', $json), 0); } - $o->addRepresentation($r, 0); + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/MicrotimePlugin.php b/system/ThirdParty/Kint/Parser/MicrotimePlugin.php index 9531bbe73121..4ebabae88327 100644 --- a/system/ThirdParty/Kint/Parser/MicrotimePlugin.php +++ b/system/ThirdParty/Kint/Parser/MicrotimePlugin.php @@ -27,15 +27,16 @@ namespace Kint\Parser; -use Kint\Zval\Representation\MicrotimeRepresentation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\MicrotimeValue; +use Kint\Value\Representation\MicrotimeRepresentation; -class MicrotimePlugin extends AbstractPlugin +class MicrotimePlugin extends AbstractPlugin implements PluginCompleteInterface { - private static $last = null; - private static $start = null; - private static $times = 0; - private static $group = 0; + private static ?array $last = null; + private static ?float $start = null; + private static int $times = 0; + private static ?string $group = null; public function getTypes(): array { @@ -47,22 +48,24 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (0 !== $o->depth) { - return; + $c = $v->getContext(); + + if ($c->getDepth() > 0) { + return $v; } if (\is_string($var)) { - if ('microtime()' !== $o->name || !\preg_match('/^0\\.[0-9]{8} [0-9]{10}$/', $var)) { - return; + if ('microtime()' !== $c->getName() || !\preg_match('/^0\\.[0-9]{8} [0-9]{10}$/', $var)) { + return $v; } $usec = (int) \substr($var, 2, 6); $sec = (int) \substr($var, 11, 10); } else { - if ('microtime(...)' !== $o->name) { - return; + if ('microtime(...)' !== $c->getName()) { + return $v; } $sec = (int) \floor($var); @@ -85,23 +88,38 @@ public function parse(&$var, Value &$o, int $trigger): void if (null !== $lap) { $total = $time - self::$start; - $r = new MicrotimeRepresentation($sec, $usec, self::$group, $lap, $total, self::$times); + $r = new MicrotimeRepresentation($sec, $usec, self::getGroup(), $lap, $total, self::$times); } else { - $r = new MicrotimeRepresentation($sec, $usec, self::$group); + $r = new MicrotimeRepresentation($sec, $usec, self::getGroup()); } - $r->contents = $var; - $r->implicit_label = true; - $o->removeRepresentation($o->value); - $o->addRepresentation($r); - $o->hints[] = 'microtime'; + $out = new MicrotimeValue($v); + $out->removeRepresentation('contents'); + $out->addRepresentation($r); + + return $out; } + /** @psalm-api */ public static function clean(): void { self::$last = null; self::$start = null; self::$times = 0; - ++self::$group; + self::newGroup(); + } + + private static function getGroup(): string + { + if (null === self::$group) { + return self::newGroup(); + } + + return self::$group; + } + + private static function newGroup(): string + { + return self::$group = \bin2hex(\random_bytes(4)); } } diff --git a/system/ThirdParty/Kint/Parser/MysqliPlugin.php b/system/ThirdParty/Kint/Parser/MysqliPlugin.php index 22a23a901ccb..51195bf306cc 100644 --- a/system/ThirdParty/Kint/Parser/MysqliPlugin.php +++ b/system/ThirdParty/Kint/Parser/MysqliPlugin.php @@ -27,9 +27,11 @@ namespace Kint\Parser; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\PropertyContext; +use Kint\Value\InstanceValue; +use Kint\Value\Representation\ContainerRepresentation; use mysqli; -use ReflectionClass; use Throwable; /** @@ -38,24 +40,24 @@ * Due to the way mysqli is implemented in PHP, this will cause * warnings on certain mysqli objects if screaming is enabled. */ -class MysqliPlugin extends AbstractPlugin +class MysqliPlugin extends AbstractPlugin implements PluginCompleteInterface { // These 'properties' are actually globals - protected $always_readable = [ + public const ALWAYS_READABLE = [ 'client_version' => true, 'connect_errno' => true, 'connect_error' => true, ]; // These are readable on empty mysqli objects, but not on failed connections - protected $empty_readable = [ + public const EMPTY_READABLE = [ 'client_info' => true, 'errno' => true, 'error' => true, ]; // These are only readable on connected mysqli objects - protected $connected_readable = [ + public const CONNECTED_READABLE = [ 'affected_rows' => true, 'error_list' => true, 'field_count' => true, @@ -80,20 +82,33 @@ public function getTriggers(): int return Parser::TRIGGER_COMPLETE; } - public function parse(&$var, Value &$o, int $trigger): void + /** + * Before 8.1: Properties were nulls when cast to array + * After 8.1: Properties are readonly and uninitialized when cast to array (Aka missing). + */ + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$var instanceof mysqli) { - return; + if (!$var instanceof mysqli || !$v instanceof InstanceValue) { + return $v; } - /** @psalm-var ?string $var->sqlstate */ + $props = $v->getRepresentation('properties'); + + if (!$props instanceof ContainerRepresentation) { + return $v; + } + + /** + * @psalm-var ?string $var->sqlstate + * @psalm-var ?string $var->client_info + * Psalm bug #4502 + */ try { $connected = \is_string(@$var->sqlstate); } catch (Throwable $t) { $connected = false; } - /** @psalm-var ?string $var->client_info */ try { $empty = !$connected && \is_string(@$var->client_info); } catch (Throwable $t) { // @codeCoverageIgnore @@ -102,93 +117,60 @@ public function parse(&$var, Value &$o, int $trigger): void $empty = false; // @codeCoverageIgnore } - foreach ($o->value->contents as $key => $obj) { - if (isset($this->connected_readable[$obj->name])) { + $parser = $this->getParser(); + + $new_contents = []; + + foreach ($props->getContents() as $key => $obj) { + $new_contents[$key] = $obj; + + $c = $obj->getContext(); + + if (!$c instanceof PropertyContext) { + continue; + } + + if (isset(self::CONNECTED_READABLE[$c->getName()])) { + $c->readonly = KINT_PHP81; if (!$connected) { // No failed connections after PHP 8.1 continue; // @codeCoverageIgnore } - } elseif (isset($this->empty_readable[$obj->name])) { + } elseif (isset(self::EMPTY_READABLE[$c->getName()])) { + $c->readonly = KINT_PHP81; // No failed connections after PHP 8.1 if (!$connected && !$empty) { // @codeCoverageIgnore continue; // @codeCoverageIgnore } - } elseif (!isset($this->always_readable[$obj->name])) { - continue; + } elseif (!isset(self::ALWAYS_READABLE[$c->getName()])) { + continue; // @codeCoverageIgnore } - if ('null' !== $obj->type) { - continue; - } - - // @codeCoverageIgnoreStart - // All of this is irellevant after 8.1, - // we have separate logic for that below - - $param = $var->{$obj->name}; + $c->readonly = KINT_PHP81; - if (null === $param) { + // Only handle unparsed properties + if ((KINT_PHP81 ? 'uninitialized' : 'null') !== $obj->getType()) { continue; } - $base = Value::blank($obj->name, $obj->access_path); + $param = $var->{$c->getName()}; - $base->depth = $obj->depth; - $base->owner_class = $obj->owner_class; - $base->operator = $obj->operator; - $base->access = $obj->access; - $base->reference = $obj->reference; - - $o->value->contents[$key] = $this->parser->parse($param, $base); + // If it really was a null + if (!KINT_PHP81 && null === $param) { + continue; // @codeCoverageIgnore + } - // @codeCoverageIgnoreEnd + $new_contents[$key] = $parser->parse($param, $c); } - // PHP81 returns an empty array when casting a mysqli instance - if (KINT_PHP81) { - $r = new ReflectionClass(mysqli::class); - - $basepropvalues = []; - - foreach ($r->getProperties() as $prop) { - if ($prop->isStatic()) { - continue; // @codeCoverageIgnore - } + $new_contents = \array_values($new_contents); - $pname = $prop->getName(); - $param = null; - - if (isset($this->connected_readable[$pname])) { - if ($connected) { - $param = $var->{$pname}; - } - } else { - $param = $var->{$pname}; - } + $v->setChildren($new_contents); - $child = new Value(); - $child->depth = $o->depth + 1; - $child->owner_class = mysqli::class; - $child->operator = Value::OPERATOR_OBJECT; - $child->name = $pname; - - if ($prop->isPublic()) { - $child->access = Value::ACCESS_PUBLIC; - } elseif ($prop->isProtected()) { // @codeCoverageIgnore - $child->access = Value::ACCESS_PROTECTED; // @codeCoverageIgnore - } elseif ($prop->isPrivate()) { // @codeCoverageIgnore - $child->access = Value::ACCESS_PRIVATE; // @codeCoverageIgnore - } - - // We only do base mysqli properties so we don't need to worry about complex names - if ($this->parser->childHasPath($o, $child)) { - $child->access_path .= $o->access_path.'->'.$child->name; - } - - $basepropvalues[] = $this->parser->parse($param, $child); - } - - $o->value->contents = \array_merge($basepropvalues, $o->value->contents); + if ($new_contents) { + $v->replaceRepresentation(new ContainerRepresentation('Properties', $new_contents)); } + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/Parser.php b/system/ThirdParty/Kint/Parser/Parser.php index f044a9de913f..b609edb08c97 100644 --- a/system/ThirdParty/Kint/Parser/Parser.php +++ b/system/ThirdParty/Kint/Parser/Parser.php @@ -29,16 +29,33 @@ use DomainException; use Exception; -use Kint\Zval\BlobValue; -use Kint\Zval\InstanceValue; -use Kint\Zval\Representation\Representation; -use Kint\Zval\ResourceValue; -use Kint\Zval\Value; +use InvalidArgumentException; +use Kint\Utils; +use Kint\Value\AbstractValue; +use Kint\Value\ArrayValue; +use Kint\Value\ClosedResourceValue; +use Kint\Value\Context\ArrayContext; +use Kint\Value\Context\ClassDeclaredContext; +use Kint\Value\Context\ClassOwnedContext; +use Kint\Value\Context\ContextInterface; +use Kint\Value\Context\PropertyContext; +use Kint\Value\FixedWidthValue; +use Kint\Value\InstanceValue; +use Kint\Value\Representation\ContainerRepresentation; +use Kint\Value\Representation\StringRepresentation; +use Kint\Value\ResourceValue; +use Kint\Value\StringValue; +use Kint\Value\UninitializedValue; +use Kint\Value\UnknownValue; +use Kint\Value\VirtualValue; +use ReflectionClass; use ReflectionObject; use ReflectionProperty; -use stdClass; -use TypeError; +use ReflectionReference; +/** + * @psalm-type ParserTrigger int-mask-of + */ class Parser { /** @@ -52,44 +69,48 @@ class Parser * DEPTH_LIMIT: After parsing cancelled by depth limit * COMPLETE: SUCCESS | RECURSION | DEPTH_LIMIT * - * While a plugin's getTriggers may return any of these + * While a plugin's getTriggers may return any of these only one should + * be given to the plugin when PluginInterface::parse is called */ public const TRIGGER_NONE = 0; - public const TRIGGER_BEGIN = 1; - public const TRIGGER_SUCCESS = 2; - public const TRIGGER_RECURSION = 4; - public const TRIGGER_DEPTH_LIMIT = 8; - public const TRIGGER_COMPLETE = 14; - - protected $caller_class; - protected $depth_limit = 0; - protected $marker; - protected $object_hashes = []; - protected $parse_break = false; - protected $plugins = []; + public const TRIGGER_BEGIN = 1 << 0; + public const TRIGGER_SUCCESS = 1 << 1; + public const TRIGGER_RECURSION = 1 << 2; + public const TRIGGER_DEPTH_LIMIT = 1 << 3; + public const TRIGGER_COMPLETE = self::TRIGGER_SUCCESS | self::TRIGGER_RECURSION | self::TRIGGER_DEPTH_LIMIT; + + /** @psalm-var ?class-string */ + protected ?string $caller_class; + protected int $depth_limit = 0; + protected array $array_ref_stack = []; + protected array $object_hashes = []; + protected array $plugins = []; /** * @param int $depth_limit Maximum depth to parse data * @param ?string $caller Caller class name + * + * @psalm-param ?class-string $caller */ - public function __construct(int $depth_limit = 0, string $caller = null) + public function __construct(int $depth_limit = 0, ?string $caller = null) { - $this->marker = "kint\0".\random_bytes(16); - $this->depth_limit = $depth_limit; $this->caller_class = $caller; } /** * Set the caller class. + * + * @psalm-param ?class-string $caller */ - public function setCallerClass(string $caller = null): void + public function setCallerClass(?string $caller = null): void { $this->noRecurseCall(); $this->caller_class = $caller; } + /** @psalm-return ?class-string */ public function getCallerClass(): ?string { return $this->caller_class; @@ -116,58 +137,67 @@ public function getDepthLimit(): int * Parses a variable into a Kint object structure. * * @param mixed &$var The input variable - * @param Value $o The base object */ - public function parse(&$var, Value $o): Value + public function parse(&$var, ContextInterface $c): AbstractValue { - $o->type = \strtolower(\gettype($var)); + $type = \strtolower(\gettype($var)); - if (!$this->applyPlugins($var, $o, self::TRIGGER_BEGIN)) { - return $o; + if ($v = $this->applyPluginsBegin($var, $c, $type)) { + return $v; } - switch ($o->type) { + switch ($type) { case 'array': - return $this->parseArray($var, $o); + return $this->parseArray($var, $c); case 'boolean': case 'double': case 'integer': case 'null': - return $this->parseGeneric($var, $o); + return $this->parseFixedWidth($var, $c); case 'object': - return $this->parseObject($var, $o); + return $this->parseObject($var, $c); case 'resource': - return $this->parseResource($var, $o); + return $this->parseResource($var, $c); case 'string': - return $this->parseString($var, $o); - case 'unknown type': + return $this->parseString($var, $c); case 'resource (closed)': + return $this->parseResourceClosed($var, $c); + + case 'unknown type': // @codeCoverageIgnore default: - return $this->parseResourceClosed($var, $o); + // These should never happen. Unknown is resource (closed) from old + // PHP versions and there shouldn't be any other types. + return $this->parseUnknown($var, $c); // @codeCoverageIgnore } } - public function addPlugin(PluginInterface $p): bool + public function addPlugin(PluginInterface $p): void { if (!$types = $p->getTypes()) { - return false; + return; } if (!$triggers = $p->getTriggers()) { - return false; + return; + } + + if ($triggers & self::TRIGGER_BEGIN && !$p instanceof PluginBeginInterface) { + throw new InvalidArgumentException('Parsers triggered on begin must implement PluginBeginInterface'); + } + + if ($triggers & self::TRIGGER_COMPLETE && !$p instanceof PluginCompleteInterface) { + throw new InvalidArgumentException('Parsers triggered on completion must implement PluginCompleteInterface'); } $p->setParser($this); foreach ($types as $type) { - if (!isset($this->plugins[$type])) { - $this->plugins[$type] = [ - self::TRIGGER_BEGIN => [], - self::TRIGGER_SUCCESS => [], - self::TRIGGER_RECURSION => [], - self::TRIGGER_DEPTH_LIMIT => [], - ]; - } + $this->plugins[$type] ??= [ + self::TRIGGER_BEGIN => [], + self::TRIGGER_SUCCESS => [], + self::TRIGGER_RECURSION => [], + self::TRIGGER_DEPTH_LIMIT => [], + ]; foreach ($this->plugins[$type] as $trigger => &$pool) { if ($triggers & $trigger) { @@ -175,8 +205,6 @@ public function addPlugin(PluginInterface $p): bool } } } - - return true; } public function clearPlugins(): void @@ -184,61 +212,6 @@ public function clearPlugins(): void $this->plugins = []; } - public function haltParse(): void - { - $this->parse_break = true; - } - - public function childHasPath(InstanceValue $parent, Value $child): bool - { - if ('__PHP_Incomplete_Class' === $parent->classname) { - return false; - } - - if ('object' === $parent->type && (null !== $parent->access_path || $child->static || $child->const)) { - if (Value::ACCESS_PUBLIC === $child->access) { - return true; - } - - if (Value::ACCESS_PRIVATE === $child->access && $this->caller_class) { - if ($this->caller_class === $child->owner_class) { - return true; - } - } elseif (Value::ACCESS_PROTECTED === $child->access && $this->caller_class) { - if ($this->caller_class === $child->owner_class) { - return true; - } - - if (\is_subclass_of($this->caller_class, $child->owner_class)) { - return true; - } - - if (\is_subclass_of($child->owner_class, $this->caller_class)) { - return true; - } - } - } - - return false; - } - - /** - * Returns an array without the recursion marker in it. - * - * DO NOT pass an array that has had it's marker removed back - * into the parser, it will result in an extra recursion - * - * @param array $array Array potentially containing a recursion marker - * - * @return array Array with recursion marker removed - */ - public function getCleanArray(array $array): array - { - unset($array[$this->marker]); - - return $array; - } - protected function noRecurseCall(): void { $bt = \debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS); @@ -259,397 +232,360 @@ protected function noRecurseCall(): void } /** - * @param null|bool|float|int &$var + * @psalm-param null|bool|float|int &$var */ - private function parseGeneric(&$var, Value $o): Value + private function parseFixedWidth(&$var, ContextInterface $c): AbstractValue { - $rep = new Representation('Contents'); - $rep->contents = $var; - $rep->implicit_label = true; - $o->addRepresentation($rep); - $o->value = $rep; - - $this->applyPlugins($var, $o, self::TRIGGER_SUCCESS); + $v = new FixedWidthValue($c, $var); - return $o; + return $this->applyPluginsComplete($var, $v, self::TRIGGER_SUCCESS); } - /** - * Parses a string into a Kint BlobValue structure. - * - * @param string &$var The input variable - * @param Value $o The base object - */ - private function parseString(string &$var, Value $o): Value + private function parseString(string &$var, ContextInterface $c): AbstractValue { - $string = new BlobValue(); - $string->transplant($o); - $string->encoding = BlobValue::detectEncoding($var); - $string->size = \strlen($var); + $string = new StringValue($c, $var, Utils::detectEncoding($var)); - $rep = new Representation('Contents'); - $rep->contents = $var; - $rep->implicit_label = true; - - $string->addRepresentation($rep); - $string->value = $rep; - - $this->applyPlugins($var, $string, self::TRIGGER_SUCCESS); + if (false !== $string->getEncoding() && \strlen($var)) { + $string->addRepresentation(new StringRepresentation('Contents', $var, null, true)); + } - return $string; + return $this->applyPluginsComplete($var, $string, self::TRIGGER_SUCCESS); } - /** - * Parses an array into a Kint object structure. - * - * @param array &$var The input variable - * @param Value $o The base object - */ - private function parseArray(array &$var, Value $o): Value + private function parseArray(array &$var, ContextInterface $c): AbstractValue { - $array = new Value(); - $array->transplant($o); - $array->size = \count($var); + $size = \count($var); + $contents = []; + $parentRef = ReflectionReference::fromArrayElement([&$var], 0)->getId(); - if (isset($var[$this->marker])) { - --$array->size; - $array->hints[] = 'recursion'; + if (isset($this->array_ref_stack[$parentRef])) { + $array = new ArrayValue($c, $size, $contents); + $array->flags |= AbstractValue::FLAG_RECURSION; - $this->applyPlugins($var, $array, self::TRIGGER_RECURSION); - - return $array; + return $this->applyPluginsComplete($var, $array, self::TRIGGER_RECURSION); } - $rep = new Representation('Contents'); - $rep->implicit_label = true; - $array->addRepresentation($rep); - $array->value = $rep; + $this->array_ref_stack[$parentRef] = true; - if (!$array->size) { - $this->applyPlugins($var, $array, self::TRIGGER_SUCCESS); + $cdepth = $c->getDepth(); + $ap = $c->getAccessPath(); - return $array; - } + if ($size > 0 && $this->depth_limit && $cdepth >= $this->depth_limit) { + $array = new ArrayValue($c, $size, $contents); + $array->flags |= AbstractValue::FLAG_DEPTH_LIMIT; - if ($this->depth_limit && $o->depth >= $this->depth_limit) { - $array->hints[] = 'depth_limit'; + $array = $this->applyPluginsComplete($var, $array, self::TRIGGER_DEPTH_LIMIT); - $this->applyPlugins($var, $array, self::TRIGGER_DEPTH_LIMIT); + unset($this->array_ref_stack[$parentRef]); return $array; } - $copy = \array_values($var); - - // It's really really hard to access numeric string keys in arrays, - // and it's really really hard to access integer properties in - // objects, so we just use array_values and index by counter to get - // at it reliably for reference testing. This also affects access - // paths since it's pretty much impossible to access these things - // without complicated stuff you should never need to do. - $i = 0; - - // Set the marker for recursion - $var[$this->marker] = $array->depth; - - $refmarker = new stdClass(); + foreach ($var as $key => $_) { + $child = new ArrayContext($key); + $child->depth = $cdepth + 1; + $child->reference = null !== ReflectionReference::fromArrayElement($var, $key); - foreach ($var as $key => &$val) { - if ($key === $this->marker) { - continue; + if (null !== $ap) { + $child->access_path = $ap.'['.\var_export($key, true).']'; } - $child = new Value(); - $child->name = $key; - $child->depth = $array->depth + 1; - $child->access = Value::ACCESS_NONE; - $child->operator = Value::OPERATOR_ARRAY; - - if (null !== $array->access_path) { - if (\is_string($key) && (string) (int) $key === $key) { - $child->access_path = 'array_values('.$array->access_path.')['.$i.']'; // @codeCoverageIgnore - } else { - $child->access_path = $array->access_path.'['.\var_export($key, true).']'; - } - } + $contents[$key] = $this->parse($var[$key], $child); + } - $stash = $val; - try { - $copy[$i] = $refmarker; - } catch (TypeError $e) { - $child->reference = true; - } - if ($val === $refmarker) { - $child->reference = true; - $val = $stash; - } + $array = new ArrayValue($c, $size, $contents); - $rep->contents[] = $this->parse($val, $child); - ++$i; + if ($contents) { + $array->addRepresentation(new ContainerRepresentation('Contents', $contents, null, true)); } - $this->applyPlugins($var, $array, self::TRIGGER_SUCCESS); - unset($var[$this->marker]); + $array = $this->applyPluginsComplete($var, $array, self::TRIGGER_SUCCESS); + + unset($this->array_ref_stack[$parentRef]); return $array; } /** - * Parses an object into a Kint InstanceValue structure. - * - * @param object &$var The input variable - * @param Value $o The base object + * @psalm-return ReflectionProperty[] */ - private function parseObject(&$var, Value $o): Value + private function getPropsOrdered(ReflectionClass $r): array { - $hash = \spl_object_hash($var); - $values = (array) $var; - - $object = new InstanceValue(); - $object->transplant($o); - $object->classname = \get_class($var); - $object->spl_object_hash = $hash; - $object->size = \count($values); - - if (KINT_PHP72) { - $object->spl_object_id = \spl_object_id($var); + if ($parent = $r->getParentClass()) { + $props = self::getPropsOrdered($parent); + } else { + $props = []; } - if (isset($this->object_hashes[$hash])) { - $object->hints[] = 'recursion'; - - $this->applyPlugins($var, $object, self::TRIGGER_RECURSION); + foreach ($r->getProperties() as $prop) { + if ($prop->isStatic()) { + continue; + } - return $object; + if ($prop->isPrivate()) { + $props[] = $prop; + } else { + $props[$prop->name] = $prop; + } } - $this->object_hashes[$hash] = $object; + return $props; + } - if ($this->depth_limit && $o->depth >= $this->depth_limit) { - $object->hints[] = 'depth_limit'; + /** + * @codeCoverageIgnore + * + * @psalm-return ReflectionProperty[] + */ + private function getPropsOrderedOld(ReflectionClass $r): array + { + $props = []; - $this->applyPlugins($var, $object, self::TRIGGER_DEPTH_LIMIT); - unset($this->object_hashes[$hash]); + foreach ($r->getProperties() as $prop) { + if ($prop->isStatic()) { + continue; + } - return $object; + $props[] = $prop; } - $reflector = new ReflectionObject($var); + while ($r = $r->getParentClass()) { + foreach ($r->getProperties(ReflectionProperty::IS_PRIVATE) as $prop) { + if ($prop->isStatic()) { + continue; + } - if ($reflector->isUserDefined()) { - $object->filename = $reflector->getFileName(); - $object->startline = $reflector->getStartLine(); + $props[] = $prop; + } } - $rep = new Representation('Properties'); + return $props; + } - $readonly = []; + private function parseObject(object &$var, ContextInterface $c): AbstractValue + { + $hash = \spl_object_hash($var); + $classname = \get_class($var); - // Reflection is both slower and more painful to use than array casting - // We only use it to identify readonly and uninitialized properties - if (KINT_PHP74 && '__PHP_Incomplete_Class' != $object->classname) { - $rprops = $reflector->getProperties(); + if (isset($this->object_hashes[$hash])) { + $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var)); + $object->flags |= AbstractValue::FLAG_RECURSION; - while ($reflector = $reflector->getParentClass()) { - $rprops = \array_merge($rprops, $reflector->getProperties(ReflectionProperty::IS_PRIVATE)); - } + return $this->applyPluginsComplete($var, $object, self::TRIGGER_RECURSION); + } - foreach ($rprops as $rprop) { - if ($rprop->isStatic()) { - continue; - } + $this->object_hashes[$hash] = true; - $rprop->setAccessible(true); + $cdepth = $c->getDepth(); + $ap = $c->getAccessPath(); - if (KINT_PHP81 && $rprop->isReadOnly()) { - if ($rprop->isPublic()) { - $readonly[$rprop->getName()] = true; - } elseif ($rprop->isProtected()) { - $readonly["\0*\0".$rprop->getName()] = true; - } elseif ($rprop->isPrivate()) { - $readonly["\0".$rprop->getDeclaringClass()->getName()."\0".$rprop->getName()] = true; - } - } + if ($this->depth_limit && $cdepth >= $this->depth_limit) { + $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var)); + $object->flags |= AbstractValue::FLAG_DEPTH_LIMIT; - if ($rprop->isInitialized($var)) { - continue; - } + $object = $this->applyPluginsComplete($var, $object, self::TRIGGER_DEPTH_LIMIT); - $undefined = null; - - $child = new Value(); - $child->type = 'undefined'; - $child->depth = $object->depth + 1; - $child->owner_class = $rprop->getDeclaringClass()->getName(); - $child->operator = Value::OPERATOR_OBJECT; - $child->name = $rprop->getName(); - $child->readonly = KINT_PHP81 && $rprop->isReadOnly(); - - if ($rprop->isPublic()) { - $child->access = Value::ACCESS_PUBLIC; - } elseif ($rprop->isProtected()) { - $child->access = Value::ACCESS_PROTECTED; - } elseif ($rprop->isPrivate()) { - $child->access = Value::ACCESS_PRIVATE; - } + unset($this->object_hashes[$hash]); - // Can't dynamically add undefined properties, so no need to use var_export - if ($this->childHasPath($object, $child)) { - $child->access_path .= $object->access_path.'->'.$child->name; - } + return $object; + } - if ($this->applyPlugins($undefined, $child, self::TRIGGER_BEGIN)) { - $this->applyPlugins($undefined, $child, self::TRIGGER_SUCCESS); - } - $rep->contents[] = $child; - } + if (KINT_PHP81) { + $props = $this->getPropsOrdered(new ReflectionObject($var)); + } else { + $props = $this->getPropsOrderedOld(new ReflectionObject($var)); // @codeCoverageIgnore } - $copy = \array_values($values); - $refmarker = new stdClass(); - $i = 0; + $values = (array) $var; + $properties = []; + + foreach ($props as $rprop) { + $rprop->setAccessible(true); + $name = $rprop->getName(); - // Reflection will not show parent classes private properties, and if a - // property was unset it will happly trigger a notice looking for it. - foreach ($values as $key => &$val) { // Casting object to array: // private properties show in the form "\0$owner_class_name\0$property_name"; // protected properties show in the form "\0*\0$property_name"; // public properties show in the form "$property_name"; // http://www.php.net/manual/en/language.types.array.php#language.types.array.casting - - $child = new Value(); - $child->depth = $object->depth + 1; - $child->owner_class = $object->classname; - $child->operator = Value::OPERATOR_OBJECT; - $child->access = Value::ACCESS_PUBLIC; - if (isset($readonly[$key])) { - $child->readonly = true; + $key = $name; + if ($rprop->isProtected()) { + $key = "\0*\0".$name; + } elseif ($rprop->isPrivate()) { + $key = "\0".$rprop->getDeclaringClass()->getName()."\0".$name; + } + $initialized = \array_key_exists($key, $values); + if ($key === (string) (int) $key) { + $key = (int) $key; } - $split_key = \explode("\0", (string) $key, 3); + if ($rprop->isDefault()) { + $child = new PropertyContext( + $name, + $rprop->getDeclaringClass()->getName(), + ClassDeclaredContext::ACCESS_PUBLIC + ); - if (3 === \count($split_key) && '' === $split_key[0]) { - $child->name = $split_key[2]; - if ('*' === $split_key[1]) { - $child->access = Value::ACCESS_PROTECTED; - } else { - $child->access = Value::ACCESS_PRIVATE; - $child->owner_class = $split_key[1]; + $child->readonly = KINT_PHP81 && $rprop->isReadOnly(); + + if ($rprop->isProtected()) { + $child->access = ClassDeclaredContext::ACCESS_PROTECTED; + } elseif ($rprop->isPrivate()) { + $child->access = ClassDeclaredContext::ACCESS_PRIVATE; + } + + if (KINT_PHP84) { + if ($rprop->isProtectedSet()) { + $child->access_set = ClassDeclaredContext::ACCESS_PROTECTED; + } elseif ($rprop->isPrivateSet()) { + $child->access_set = ClassDeclaredContext::ACCESS_PRIVATE; + } + + $hooks = $rprop->getHooks(); + if (isset($hooks['get'])) { + $child->hooks |= PropertyContext::HOOK_GET; + if ($hooks['get']->returnsReference()) { + $child->hooks |= PropertyContext::HOOK_GET_REF; + } + } + if (isset($hooks['set'])) { + $child->hooks |= PropertyContext::HOOK_SET; + + $child->hook_set_type = (string) $rprop->getSettableType(); + if ($child->hook_set_type !== (string) $rprop->getType()) { + $child->hooks |= PropertyContext::HOOK_SET_TYPE; + } elseif ('' === $child->hook_set_type) { + $child->hook_set_type = null; + } + } } - } elseif (KINT_PHP72) { - $child->name = (string) $key; } else { - $child->name = $key; // @codeCoverageIgnore + $child = new ClassOwnedContext($name, $rprop->getDeclaringClass()->getName()); } - if ($this->childHasPath($object, $child)) { - $child->access_path = $object->access_path; + $child->reference = $initialized && null !== ReflectionReference::fromArrayElement($values, $key); + $child->depth = $cdepth + 1; - if (!KINT_PHP72 && \is_int($child->name)) { - $child->access_path = 'array_values((array) '.$child->access_path.')['.$i.']'; // @codeCoverageIgnore - } elseif (\preg_match('/^[a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*$/', $child->name)) { - $child->access_path .= '->'.$child->name; + if (null !== $ap && $child->isAccessible($this->caller_class)) { + /** @psalm-var string $child->name */ + if (Utils::isValidPhpName($child->name)) { + $child->access_path = $ap.'->'.$child->name; } else { - $child->access_path .= '->{'.\var_export((string) $child->name, true).'}'; + $child->access_path = $ap.'->{'.\var_export($child->name, true).'}'; } } - $stash = $val; - try { - $copy[$i] = $refmarker; - } catch (TypeError $e) { - $child->reference = true; - } - if ($val === $refmarker) { - $child->reference = true; - $val = $stash; + if (KINT_PHP84 && $rprop->isVirtual()) { + $properties[] = new VirtualValue($child); + } elseif (!$initialized) { + $properties[] = new UninitializedValue($child); + } else { + $properties[] = $this->parse($values[$key], $child); } + } - $rep->contents[] = $this->parse($val, $child); - ++$i; + $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var)); + if ($props) { + $object->setChildren($properties); } - $object->addRepresentation($rep); - $object->value = $rep; - $this->applyPlugins($var, $object, self::TRIGGER_SUCCESS); + if ($properties) { + $object->addRepresentation(new ContainerRepresentation('Properties', $properties)); + } + + $object = $this->applyPluginsComplete($var, $object, self::TRIGGER_SUCCESS); unset($this->object_hashes[$hash]); return $object; } /** - * Parses a resource into a Kint ResourceValue structure. - * - * @param resource &$var The input variable - * @param Value $o The base object + * @psalm-param resource $var */ - private function parseResource(&$var, Value $o): Value + private function parseResource(&$var, ContextInterface $c): AbstractValue { - $resource = new ResourceValue(); - $resource->transplant($o); - $resource->resource_type = \get_resource_type($var); + $resource = new ResourceValue($c, \get_resource_type($var)); - $this->applyPlugins($var, $resource, self::TRIGGER_SUCCESS); + $resource = $this->applyPluginsComplete($var, $resource, self::TRIGGER_SUCCESS); return $resource; } /** - * Parses a closed resource into a Kint object structure. - * - * @param mixed &$var The input variable - * @param Value $o The base object + * @psalm-param mixed $var */ - private function parseResourceClosed(&$var, Value $o): Value + private function parseResourceClosed(&$var, ContextInterface $c): AbstractValue { - $o->type = 'resource (closed)'; - $this->applyPlugins($var, $o, self::TRIGGER_SUCCESS); + $v = new ClosedResourceValue($c); - return $o; + $v = $this->applyPluginsComplete($var, $v, self::TRIGGER_SUCCESS); + + return $v; } /** - * Applies plugins for an object type. + * Catch-all for any unexpectedgettype. + * + * This should never happen. * - * @param mixed &$var variable - * @param Value $o Kint object parsed so far - * @param int $trigger The trigger to check for the plugins + * @codeCoverageIgnore * - * @return bool Continue parsing + * @psalm-param mixed $var */ - private function applyPlugins(&$var, Value &$o, int $trigger): bool + private function parseUnknown(&$var, ContextInterface $c): AbstractValue { - $break_stash = $this->parse_break; + $v = new UnknownValue($c); - /** @psalm-var bool */ - $this->parse_break = false; + $v = $this->applyPluginsComplete($var, $v, self::TRIGGER_SUCCESS); - $plugins = []; + return $v; + } - if (isset($this->plugins[$o->type][$trigger])) { - $plugins = $this->plugins[$o->type][$trigger]; - } + /** + * Applies plugins for a yet-unparsed value. + * + * @param mixed &$var The input variable + */ + private function applyPluginsBegin(&$var, ContextInterface $c, string $type): ?AbstractValue + { + $plugins = $this->plugins[$type][self::TRIGGER_BEGIN] ?? []; foreach ($plugins as $plugin) { try { - $plugin->parse($var, $o, $trigger); + if ($v = $plugin->parseBegin($var, $c)) { + return $v; + } } catch (Exception $e) { \trigger_error( - 'An exception ('.\get_class($e).') was thrown in '.$e->getFile().' on line '.$e->getLine().' while executing Kint Parser Plugin "'.\get_class($plugin).'". Error message: '.$e->getMessage(), + 'An exception ('.Utils::errorSanitizeString(\get_class($e)).') was thrown in '.$e->getFile().' on line '.$e->getLine().' while executing "'.Utils::errorSanitizeString(\get_class($plugin)).'"->parseBegin. Error message: '.Utils::errorSanitizeString($e->getMessage()), E_USER_WARNING ); } + } + + return null; + } - if ($this->parse_break) { - $this->parse_break = $break_stash; + /** + * Applies plugins for a parsed AbstractValue. + * + * @param mixed &$var The input variable + */ + private function applyPluginsComplete(&$var, AbstractValue $v, int $trigger): AbstractValue + { + $plugins = $this->plugins[$v->getType()][$trigger] ?? []; - return false; + foreach ($plugins as $plugin) { + try { + $v = $plugin->parseComplete($var, $v, $trigger); + } catch (Exception $e) { + \trigger_error( + 'An exception ('.Utils::errorSanitizeString(\get_class($e)).') was thrown in '.$e->getFile().' on line '.$e->getLine().' while executing "'.Utils::errorSanitizeString(\get_class($plugin)).'"->parseComplete. Error message: '.Utils::errorSanitizeString($e->getMessage()), + E_USER_WARNING + ); } } - $this->parse_break = $break_stash; - - return true; + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/PluginBeginInterface.php b/system/ThirdParty/Kint/Parser/PluginBeginInterface.php new file mode 100644 index 000000000000..25d72f4e1525 --- /dev/null +++ b/system/ThirdParty/Kint/Parser/PluginBeginInterface.php @@ -0,0 +1,39 @@ +getDepth()) { + $this->instance_counts = []; + $this->instance_complexity = []; + $this->instance_count_stack = []; + $this->class_complexity = []; + $this->class_count_stack = []; + } + + if (\is_object($var)) { + $hash = \spl_object_hash($var); + $this->instance_counts[$hash] ??= 0; + $this->instance_complexity[$hash] ??= 0; + $this->instance_count_stack[$hash] ??= 0; + + if (0 === $this->instance_count_stack[$hash]) { + foreach (\class_parents($var) as $class) { + $this->class_count_stack[$class] ??= 0; + ++$this->class_count_stack[$class]; + } + + foreach (\class_implements($var) as $iface) { + $this->class_count_stack[$iface] ??= 0; + ++$this->class_count_stack[$iface]; + } + } + + ++$this->instance_count_stack[$hash]; + } + + return null; + } + + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue + { + if ($v instanceof InstanceValue) { + --$this->instance_count_stack[$v->getSplObjectHash()]; + + if (0 === $this->instance_count_stack[$v->getSplObjectHash()]) { + foreach (\class_parents($var) as $class) { + --$this->class_count_stack[$class]; + } + + foreach (\class_implements($var) as $iface) { + --$this->class_count_stack[$iface]; + } + } + } + + // Don't check subs if we're in recursion or array limit + if (~$trigger & Parser::TRIGGER_SUCCESS) { + return $v; + } + + $sub_complexity = 1; + + foreach ($v->getRepresentations() as $rep) { + if ($rep instanceof ContainerRepresentation) { + foreach ($rep->getContents() as $value) { + $profile = $value->getRepresentation('profiling'); + $sub_complexity += $profile instanceof ProfileRepresentation ? $profile->complexity : 1; + } + } else { + ++$sub_complexity; + } + } + + if ($v instanceof InstanceValue) { + ++$this->instance_counts[$v->getSplObjectHash()]; + if (0 === $this->instance_count_stack[$v->getSplObjectHash()]) { + $this->instance_complexity[$v->getSplObjectHash()] += $sub_complexity; + + $this->class_complexity[$v->getClassName()] ??= 0; + $this->class_complexity[$v->getClassName()] += $sub_complexity; + + foreach (\class_parents($var) as $class) { + $this->class_complexity[$class] ??= 0; + if (0 === $this->class_count_stack[$class]) { + $this->class_complexity[$class] += $sub_complexity; + } + } + + foreach (\class_implements($var) as $iface) { + $this->class_complexity[$iface] ??= 0; + if (0 === $this->class_count_stack[$iface]) { + $this->class_complexity[$iface] += $sub_complexity; + } + } + } + } + + if (0 === $v->getContext()->getDepth()) { + $contents = []; + + \arsort($this->class_complexity); + + foreach ($this->class_complexity as $name => $complexity) { + $contents[] = new FixedWidthValue(new BaseContext($name), $complexity); + } + + if ($contents) { + $v->addRepresentation(new ContainerRepresentation('Class complexity', $contents), 0); + } + } + + $rep = new ProfileRepresentation($sub_complexity); + /** @psalm-suppress UnsupportedReferenceUsage */ + if ($v instanceof InstanceValue) { + $rep->instance_counts = &$this->instance_counts[$v->getSplObjectHash()]; + $rep->instance_complexity = &$this->instance_complexity[$v->getSplObjectHash()]; + } + + $v->addRepresentation($rep, 0); + + return $v; + } +} diff --git a/system/ThirdParty/Kint/Parser/ProxyPlugin.php b/system/ThirdParty/Kint/Parser/ProxyPlugin.php index 97050c6b4abf..e0db1b985708 100644 --- a/system/ThirdParty/Kint/Parser/ProxyPlugin.php +++ b/system/ThirdParty/Kint/Parser/ProxyPlugin.php @@ -27,25 +27,29 @@ namespace Kint\Parser; -use InvalidArgumentException; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\ContextInterface; -class ProxyPlugin implements PluginInterface +/** + * @psalm-import-type ParserTrigger from Parser + * + * @psalm-api + */ +class ProxyPlugin implements PluginBeginInterface, PluginCompleteInterface { - protected $parser; - protected $types; - protected $triggers; + protected array $types; + /** @psalm-var ParserTrigger */ + protected int $triggers; + /** @psalm-var callable */ protected $callback; + private ?Parser $parser = null; /** - * @param callable $callback + * @psalm-param ParserTrigger $triggers + * @psalm-param callable $callback */ public function __construct(array $types, int $triggers, $callback) { - if (!\is_callable($callback)) { - throw new InvalidArgumentException('ProxyPlugin callback must be callable'); - } - $this->types = $types; $this->triggers = $triggers; $this->callback = $callback; @@ -66,8 +70,23 @@ public function getTriggers(): int return $this->triggers; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseBegin(&$var, ContextInterface $c): ?AbstractValue + { + return \call_user_func_array($this->callback, [ + &$var, + $c, + Parser::TRIGGER_BEGIN, + $this->parser, + ]); + } + + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - \call_user_func_array($this->callback, [&$var, &$o, $trigger, $this->parser]); + return \call_user_func_array($this->callback, [ + &$var, + $v, + $trigger, + $this->parser, + ]); } } diff --git a/system/ThirdParty/Kint/Parser/SerializePlugin.php b/system/ThirdParty/Kint/Parser/SerializePlugin.php index 4991087848e6..a23aaae1af2c 100644 --- a/system/ThirdParty/Kint/Parser/SerializePlugin.php +++ b/system/ThirdParty/Kint/Parser/SerializePlugin.php @@ -27,10 +27,13 @@ namespace Kint\Parser; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\Representation\ValueRepresentation; +use Kint\Value\UninitializedValue; -class SerializePlugin extends AbstractPlugin +/** @psalm-api */ +class SerializePlugin extends AbstractPlugin implements PluginCompleteInterface { /** * Disables automatic unserialization on arrays and objects. @@ -43,13 +46,11 @@ class SerializePlugin extends AbstractPlugin * * The natural way to stop that from happening is to just refuse to unserialize * stuff by default. Which is what we're doing for anything that's not scalar. - * - * @var bool */ - public static $safe_mode = true; + public static bool $safe_mode = true; /** - * @var bool|class-string[] + * @psalm-var bool|class-string[] */ public static $allowed_classes = false; @@ -63,47 +64,48 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { $trimmed = \rtrim($var); if ('N;' !== $trimmed && !\preg_match('/^(?:[COabis]:\\d+[:;]|d:\\d+(?:\\.\\d+);)/', $trimmed)) { - return; + return $v; } $options = ['allowed_classes' => self::$allowed_classes]; - if (!self::$safe_mode || !\in_array($trimmed[0], ['C', 'O', 'a'], true)) { - // Suppress warnings on unserializeable variable - $data = @\unserialize($trimmed, $options); - - if (false === $data && 'b:0;' !== \substr($trimmed, 0, 4)) { - return; - } - } + $c = $v->getContext(); - $base_obj = new Value(); - $base_obj->depth = $o->depth + 1; - $base_obj->name = 'unserialize('.$o->name.')'; + $base = new BaseContext('unserialize('.$c->getName().')'); + $base->depth = $c->getDepth() + 1; - if ($o->access_path) { - $base_obj->access_path = 'unserialize('.$o->access_path; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = 'unserialize('.$ap; if (true === self::$allowed_classes) { - $base_obj->access_path .= ')'; + $base->access_path .= ')'; } else { - $base_obj->access_path .= ', '.\var_export($options, true).')'; + $base->access_path .= ', '.\var_export($options, true).')'; } } - $r = new Representation('Serialized'); - - if (isset($data)) { - $r->contents = $this->parser->parse($data, $base_obj); + if (self::$safe_mode && \in_array($trimmed[0], ['C', 'O', 'a'], true)) { + $data = new UninitializedValue($base); + $data->flags |= AbstractValue::FLAG_BLACKLIST; } else { - $base_obj->hints[] = 'blacklist'; - $r->contents = $base_obj; + // Suppress warnings on unserializeable variable + $data = @\unserialize($trimmed, $options); + + if (false === $data && 'b:0;' !== \substr($trimmed, 0, 4)) { + return $v; + } + + $data = $this->getParser()->parse($data, $base); } - $o->addRepresentation($r, 0); + $data->flags |= AbstractValue::FLAG_GENERATED; + + $v->addRepresentation(new ValueRepresentation('Serialized', $data), 0); + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/SimpleXMLElementPlugin.php b/system/ThirdParty/Kint/Parser/SimpleXMLElementPlugin.php index db6f9b9c1c2b..4511c0b76f9e 100644 --- a/system/ThirdParty/Kint/Parser/SimpleXMLElementPlugin.php +++ b/system/ThirdParty/Kint/Parser/SimpleXMLElementPlugin.php @@ -27,20 +27,39 @@ namespace Kint\Parser; -use Kint\Zval\BlobValue; -use Kint\Zval\Representation\Representation; -use Kint\Zval\SimpleXMLElementValue; -use Kint\Zval\Value; +use Kint\Utils; +use Kint\Value\AbstractValue; +use Kint\Value\Context\ArrayContext; +use Kint\Value\Context\BaseContext; +use Kint\Value\Context\ClassOwnedContext; +use Kint\Value\Context\ContextInterface; +use Kint\Value\Representation\ContainerRepresentation; +use Kint\Value\Representation\ValueRepresentation; +use Kint\Value\SimpleXMLElementValue; use SimpleXMLElement; -class SimpleXMLElementPlugin extends AbstractPlugin +class SimpleXMLElementPlugin extends AbstractPlugin implements PluginBeginInterface { /** * Show all properties and methods. - * - * @var bool */ - public static $verbose = false; + public static bool $verbose = false; + + protected ClassMethodsPlugin $methods_plugin; + + public function __construct(Parser $parser) + { + parent::__construct($parser); + + $this->methods_plugin = new ClassMethodsPlugin($parser); + } + + public function setParser(Parser $p): void + { + parent::setParser($p); + + $this->methods_plugin->setParser($p); + } public function getTypes(): array { @@ -49,173 +68,228 @@ public function getTypes(): array public function getTriggers(): int { - return Parser::TRIGGER_SUCCESS; + // SimpleXMLElement is a weirdo. No recursion (Or rather everything is + // recursion) and depth limit will have to be handled manually anyway. + return Parser::TRIGGER_BEGIN; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseBegin(&$var, ContextInterface $c): ?AbstractValue { if (!$var instanceof SimpleXMLElement) { - return; + return null; } - if (!self::$verbose) { - $o->removeRepresentation('properties'); - $o->removeRepresentation('iterator'); - $o->removeRepresentation('methods'); - } + return $this->parseElement($var, $c); + } + + protected function parseElement(SimpleXMLElement &$var, ContextInterface $c): SimpleXMLElementValue + { + $parser = $this->getParser(); + $pdepth = $parser->getDepthLimit(); + $cdepth = $c->getDepth(); + + $depthlimit = $pdepth && $cdepth >= $pdepth; + $has_children = self::hasChildElements($var); - // An invalid SimpleXMLElement can gum up the works with - // warnings if we call stuff children/attributes on it. - if (!$var) { - $o->size = null; + if ($depthlimit && $has_children) { + $x = new SimpleXMLElementValue($c, $var, [], null); + $x->flags |= AbstractValue::FLAG_DEPTH_LIMIT; - return; + return $x; } - $x = new SimpleXMLElementValue(); - $x->transplant($o); + $children = $this->getChildren($c, $var); + $attributes = $this->getAttributes($c, $var); + $toString = (string) $var; + $string_body = !$has_children && \strlen($toString); + + $x = new SimpleXMLElementValue($c, $var, $children, \strlen($toString) ? $toString : null); + + if (self::$verbose) { + $x = $this->methods_plugin->parseComplete($var, $x, Parser::TRIGGER_SUCCESS); + } - $namespaces = \array_merge([null], $var->getDocNamespaces()); + if ($attributes) { + $x->addRepresentation(new ContainerRepresentation('Attributes', $attributes), 0); + } - // Attributes - $a = new Representation('Attributes'); + if ($string_body) { + $base = new BaseContext('(string) '.$c->getName()); + $base->depth = $cdepth + 1; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = '(string) '.$ap; + } - $base_obj = new Value(); - $base_obj->depth = $x->depth; + $toString = $parser->parse($toString, $base); - if ($x->access_path) { - $base_obj->access_path = '(string) '.$x->access_path; + $x->addRepresentation(new ValueRepresentation('toString', $toString, null, true), 0); } - // Attributes are strings. If we're too deep set the - // depth limit to enable parsing them, but no deeper. - if ($this->parser->getDepthLimit() && $this->parser->getDepthLimit() - 2 < $base_obj->depth) { - $base_obj->depth = $this->parser->getDepthLimit() - 2; + if ($children) { + $x->addRepresentation(new ContainerRepresentation('Children', $children), 0); } - $attribs = []; + return $x; + } - foreach ($namespaces as $nsAlias => $nsUrl) { - if ($nsAttribs = $var->attributes($nsUrl)) { - $cleanAttribs = []; + /** @psalm-return list */ + protected function getAttributes(ContextInterface $c, SimpleXMLElement $var): array + { + $parser = $this->getParser(); + $namespaces = \array_merge(['' => null], $var->getDocNamespaces()); + + $cdepth = $c->getDepth(); + $ap = $c->getAccessPath(); + + $contents = []; + + foreach ($namespaces as $nsAlias => $_) { + if ((bool) $nsAttribs = $var->attributes($nsAlias, true)) { foreach ($nsAttribs as $name => $attrib) { - $cleanAttribs[(string) $name] = $attrib; - } + $obj = new ArrayContext($name); + $obj->depth = $cdepth + 1; - if (null === $nsUrl) { - $obj = clone $base_obj; - if ($obj->access_path) { - $obj->access_path .= '->attributes()'; + if (null !== $ap) { + $obj->access_path = '(string) '.$ap; + if ('' !== $nsAlias) { + $obj->access_path .= '->attributes('.\var_export($nsAlias, true).', true)'; + } + $obj->access_path .= '['.\var_export($name, true).']'; } - $a->contents = $this->parser->parse($cleanAttribs, $obj)->value->contents; - } else { - $obj = clone $base_obj; - if ($obj->access_path) { - $obj->access_path .= '->attributes('.\var_export($nsAlias, true).', true)'; + if ('' !== $nsAlias) { + $obj->name = $nsAlias.':'.$obj->name; } - $cleanAttribs = $this->parser->parse($cleanAttribs, $obj)->value->contents; + $string = (string) $attrib; + $attribute = $parser->parse($string, $obj); - foreach ($cleanAttribs as $attribute) { - $attribute->name = $nsAlias.':'.$attribute->name; - $a->contents[] = $attribute; - } + $contents[] = $attribute; } } } - if ($a->contents) { - $x->addRepresentation($a, 0); - } + return $contents; + } - // Children - $c = new Representation('Children'); + /** + * Alright kids, let's learn about SimpleXMLElement::children! + * children can take a namespace url or alias and provide a list of + * child nodes. This is great since just accessing the members through + * properties doesn't work on SimpleXMLElement when they have a + * namespace at all! + * + * Unfortunately SimpleXML decided to go the retarded route of + * categorizing elements by their tag name rather than by their local + * name (to put it in Dom terms) so if you have something like this: + * + * + * + * + * + * + * + * * children(null) will get the first 2 results + * * children('', true) will get the first 2 results + * * children('http://localhost/') will get the last 2 results + * * children('localhost', true) will get the last result + * + * So let's just give up and stick to aliases because fuck that mess! + * + * @psalm-return list + */ + protected function getChildren(ContextInterface $c, SimpleXMLElement $var): array + { + $namespaces = \array_merge(['' => null], $var->getDocNamespaces()); - foreach ($namespaces as $nsAlias => $nsUrl) { - // This is doubling items because of the root namespace - // and the implicit namespace on its children. - $thisNs = $var->getNamespaces(); - if (isset($thisNs['']) && $thisNs[''] === $nsUrl) { - continue; - } + $cdepth = $c->getDepth(); + $ap = $c->getAccessPath(); - if ($nsChildren = $var->children($nsUrl)) { + $contents = []; + + foreach ($namespaces as $nsAlias => $_) { + if ((bool) $nsChildren = $var->children($nsAlias, true)) { $nsap = []; foreach ($nsChildren as $name => $child) { - $obj = new Value(); - $obj->depth = $x->depth + 1; - $obj->name = (string) $name; - if ($x->access_path) { - if (null === $nsUrl) { - $obj->access_path = $x->access_path.'->children()->'; + $base = new ClassOwnedContext((string) $name, SimpleXMLElement::class); + $base->depth = $cdepth + 1; + + if ('' !== $nsAlias) { + $base->name = $nsAlias.':'.$name; + } + + if (null !== $ap) { + if ('' === $nsAlias) { + $base->access_path = $ap.'->'; } else { - $obj->access_path = $x->access_path.'->children('.\var_export($nsAlias, true).', true)->'; + $base->access_path = $ap.'->children('.\var_export($nsAlias, true).', true)->'; } - if (\preg_match('/^[a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]+$/', (string) $name)) { - $obj->access_path .= (string) $name; + if (Utils::isValidPhpName((string) $name)) { + $base->access_path .= (string) $name; } else { - $obj->access_path .= '{'.\var_export((string) $name, true).'}'; + $base->access_path .= '{'.\var_export((string) $name, true).'}'; } - if (isset($nsap[$obj->access_path])) { - ++$nsap[$obj->access_path]; - $obj->access_path .= '['.$nsap[$obj->access_path].']'; + if (isset($nsap[$base->access_path])) { + ++$nsap[$base->access_path]; + $base->access_path .= '['.$nsap[$base->access_path].']'; } else { - $nsap[$obj->access_path] = 0; + $nsap[$base->access_path] = 0; } } - $value = $this->parser->parse($child, $obj); - - if ($value->access_path && 'string' === $value->type) { - $value->access_path = '(string) '.$value->access_path; - } - - $c->contents[] = $value; + $v = $this->parseElement($child, $base); + $v->flags |= AbstractValue::FLAG_GENERATED; + $contents[] = $v; } } } - $x->size = \count($c->contents); - - if ($x->size) { - $x->addRepresentation($c, 0); - } else { - $x->size = null; - - if (\strlen((string) $var)) { - $base_obj = new BlobValue(); - $base_obj->depth = $x->depth + 1; - $base_obj->name = $x->name; - if ($x->access_path) { - $base_obj->access_path = '(string) '.$x->access_path; - } - - $value = (string) $var; - - $s = $this->parser->parse($value, $base_obj); - $srep = $s->getRepresentation('contents'); - $svalrep = $s->value && 'contents' == $s->value->getName() ? $s->value : null; - - if ($srep || $svalrep) { - $x->setIsStringValue(true); - $x->value = $srep ?: $svalrep; - - if ($srep) { - $x->replaceRepresentation($srep, 0); - } - } + return $contents; + } - $reps = \array_reverse($s->getRepresentations()); + /** + * More SimpleXMLElement bullshit. + * + * If we want to know if the element contains text we can cast to string. + * Except if it contains text mixed with elements simplexml for some stupid + * reason decides to concatenate the text from between those elements + * rather than all the text in the hierarchy... + * + * So we have NO way of getting text nodes between elements, but we can + * still tell if we have elements right? If we have elements we assume it's + * not a string and call it a day! + * + * Well if you cast the element to an array attributes will be on it so + * you'd have to remove that key, and if it's a string it'll also have the + * 0 index used for the string contents too... + * + * Wait, can we use the 0 index to tell if it's a string? Nope! CDATA + * doesn't show up AT ALL when casting to anything but string, and we'll + * still get those concatenated strings of mostly whitespace if we just do + * (string) and check the length. + * + * Luckily, I found the only way to do this reliably is through children(). + * We still have to loop through all the namespaces and see if there's a + * match but then we have the problem of the attributes showing up again... + * + * Or at least that's what var_dump says. And when we cast the result to + * bool it's true too... But if we cast it to array then it's suddenly empty! + * + * Long story short the function below is the only way to reliably check if + * a SimpleXMLElement has children + */ + protected static function hasChildElements(SimpleXMLElement $var): bool + { + $namespaces = \array_merge(['' => null], $var->getDocNamespaces()); - foreach ($reps as $rep) { - $x->addRepresentation($rep, 0); - } + foreach ($namespaces as $nsAlias => $_) { + if ((array) $var->children($nsAlias, true)) { + return true; } } - $o = $x; + return false; } } diff --git a/system/ThirdParty/Kint/Parser/SplFileInfoPlugin.php b/system/ThirdParty/Kint/Parser/SplFileInfoPlugin.php index 696f36007013..c5eea6c1e8d5 100644 --- a/system/ThirdParty/Kint/Parser/SplFileInfoPlugin.php +++ b/system/ThirdParty/Kint/Parser/SplFileInfoPlugin.php @@ -27,12 +27,14 @@ namespace Kint\Parser; -use Kint\Zval\Representation\SplFileInfoRepresentation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\InstanceValue; +use Kint\Value\Representation\SplFileInfoRepresentation; +use Kint\Value\SplFileInfoValue; use SplFileInfo; use SplFileObject; -class SplFileInfoPlugin extends AbstractPlugin +class SplFileInfoPlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -41,17 +43,26 @@ public function getTypes(): array public function getTriggers(): int { - return Parser::TRIGGER_COMPLETE; + return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { + // SplFileObject throws exceptions in normal use in places SplFileInfo doesn't if (!$var instanceof SplFileInfo || $var instanceof SplFileObject) { - return; + return $v; } - $r = new SplFileInfoRepresentation(clone $var); - $o->addRepresentation($r, 0); - $o->size = $r->getSize(); + if (!$v instanceof InstanceValue) { + return $v; + } + + $out = new SplFileInfoValue($v->getContext(), $var); + $out->setChildren($v->getChildren()); + $out->flags = $v->flags; + $out->addRepresentation(new SplFileInfoRepresentation(clone $var)); + $out->appendRepresentations($v->getRepresentations()); + + return $out; } } diff --git a/system/ThirdParty/Kint/Parser/StreamPlugin.php b/system/ThirdParty/Kint/Parser/StreamPlugin.php index 748b53c51997..fdbc2573bc5a 100644 --- a/system/ThirdParty/Kint/Parser/StreamPlugin.php +++ b/system/ThirdParty/Kint/Parser/StreamPlugin.php @@ -27,12 +27,13 @@ namespace Kint\Parser; -use Kint\Zval\Representation\Representation; -use Kint\Zval\ResourceValue; -use Kint\Zval\StreamValue; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\ArrayContext; +use Kint\Value\ResourceValue; +use Kint\Value\StreamValue; +use TypeError; -class StreamPlugin extends AbstractPlugin +class StreamPlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -44,40 +45,44 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$o instanceof ResourceValue || 'stream' !== $o->resource_type) { - return; + if (!$v instanceof ResourceValue) { + return $v; } // Doublecheck that the resource is open before we get the metadata if (!\is_resource($var)) { - return; + return $v; } - $meta = \stream_get_meta_data($var); - - $rep = new Representation('Stream'); - $rep->implicit_label = true; + try { + $meta = \stream_get_meta_data($var); + } catch (TypeError $e) { + return $v; + } - $base_obj = new Value(); - $base_obj->depth = $o->depth; + $c = $v->getContext(); - if ($o->access_path) { - $base_obj->access_path = 'stream_get_meta_data('.$o->access_path.')'; - } + $parser = $this->getParser(); + $parsed_meta = []; + foreach ($meta as $key => $val) { + $base = new ArrayContext($key); + $base->depth = $c->getDepth() + 1; - $rep->contents = $this->parser->parse($meta, $base_obj); + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = 'stream_get_meta_data('.$ap.')['.\var_export($key, true).']'; + } - if (!\in_array('depth_limit', $rep->contents->hints, true)) { - $rep->contents = $rep->contents->value->contents; + $val = $parser->parse($val, $base); + $val->flags |= AbstractValue::FLAG_GENERATED; + $parsed_meta[] = $val; } - $o->addRepresentation($rep, 0); - $o->value = $rep; + $stream = new StreamValue($c, $parsed_meta, $meta['uri'] ?? null); + $stream->flags = $v->flags; + $stream->appendRepresentations($v->getRepresentations()); - $stream = new StreamValue($meta); - $stream->transplant($o); - $o = $stream; + return $stream; } } diff --git a/system/ThirdParty/Kint/Parser/TablePlugin.php b/system/ThirdParty/Kint/Parser/TablePlugin.php index 5afe75980bc8..8bbda7f7b181 100644 --- a/system/ThirdParty/Kint/Parser/TablePlugin.php +++ b/system/ThirdParty/Kint/Parser/TablePlugin.php @@ -27,8 +27,9 @@ namespace Kint\Parser; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\ArrayValue; +use Kint\Value\Representation\TableRepresentation; // Note: Interaction with ArrayLimitPlugin: // Any array limited children will be shown in tables identically to @@ -36,8 +37,11 @@ // and it's size anyway. Because ArrayLimitPlugin halts the parse on finding // a limit all other plugins including this one are stopped, so you cannot get // a tabular representation of an array that is longer than the limit. -class TablePlugin extends AbstractPlugin +class TablePlugin extends AbstractPlugin implements PluginCompleteInterface { + public static int $max_width = 300; + public static int $min_width = 2; + public function getTypes(): array { return ['array']; @@ -48,48 +52,52 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (empty($o->value->contents)) { - return; + if (!$v instanceof ArrayValue) { + return $v; } - $array = $this->parser->getCleanArray($var); - - if (\count($array) < 2) { - return; + if (\count($var) < 2) { + return $v; } // Ensure this is an array of arrays and that all child arrays have the // same keys. We don't care about their children - if there's another // "table" inside we'll just make another one down the value tab $keys = null; - foreach ($array as $elem) { - if (!\is_array($elem) || \count($elem) < 2) { - return; + foreach ($var as $elem) { + if (!\is_array($elem)) { + return $v; } if (null === $keys) { + if (\count($elem) < self::$min_width || \count($elem) > self::$max_width) { + return $v; + } + $keys = \array_keys($elem); } elseif (\array_keys($elem) !== $keys) { - return; + return $v; } } + $children = $v->getContents(); + + if (!$children) { + return $v; + } + // Ensure none of the child arrays are recursion or depth limit. We // don't care if their children are since they are the table cells - foreach ($o->value->contents as $childarray) { - if (empty($childarray->value->contents)) { - return; + foreach ($children as $childarray) { + if (!$childarray instanceof ArrayValue || empty($childarray->getContents())) { + return $v; } } - // Objects by reference for the win! We can do a copy-paste of the value - // representation contents and just slap a new hint on there and hey - // presto we have our table representation with no extra memory used! - $table = new Representation('Table'); - $table->contents = $o->value->contents; - $table->hints[] = 'table'; - $o->addRepresentation($table, 0); + $v->addRepresentation(new TableRepresentation($children), 0); + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/ThrowablePlugin.php b/system/ThirdParty/Kint/Parser/ThrowablePlugin.php index 7fd065f61910..c314fafb6c18 100644 --- a/system/ThirdParty/Kint/Parser/ThrowablePlugin.php +++ b/system/ThirdParty/Kint/Parser/ThrowablePlugin.php @@ -27,12 +27,14 @@ namespace Kint\Parser; -use Kint\Zval\Representation\SourceRepresentation; -use Kint\Zval\ThrowableValue; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\InstanceValue; +use Kint\Value\Representation\SourceRepresentation; +use Kint\Value\ThrowableValue; +use RuntimeException; use Throwable; -class ThrowablePlugin extends AbstractPlugin +class ThrowablePlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -44,18 +46,22 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$var instanceof Throwable) { - return; + if (!$var instanceof Throwable || !$v instanceof InstanceValue) { + return $v; } - $throw = new ThrowableValue($var); - $throw->transplant($o); - $r = new SourceRepresentation($var->getFile(), $var->getLine()); - $r->showfilename = true; - $throw->addRepresentation($r, 0); + $throw = new ThrowableValue($v->getContext(), $var); + $throw->setChildren($v->getChildren()); + $throw->flags = $v->flags; + $throw->appendRepresentations($v->getRepresentations()); - $o = $throw; + try { + $throw->addRepresentation(new SourceRepresentation($var->getFile(), $var->getLine(), null, true), 0); + } catch (RuntimeException $e) { + } + + return $throw; } } diff --git a/system/ThirdParty/Kint/Parser/TimestampPlugin.php b/system/ThirdParty/Kint/Parser/TimestampPlugin.php index 9136ca800d49..da3506fe7c62 100644 --- a/system/ThirdParty/Kint/Parser/TimestampPlugin.php +++ b/system/ThirdParty/Kint/Parser/TimestampPlugin.php @@ -27,11 +27,15 @@ namespace Kint\Parser; -use Kint\Zval\Value; +use DateTimeImmutable; +use Kint\Value\AbstractValue; +use Kint\Value\FixedWidthValue; +use Kint\Value\Representation\StringRepresentation; +use Kint\Value\StringValue; -class TimestampPlugin extends AbstractPlugin +class TimestampPlugin extends AbstractPlugin implements PluginCompleteInterface { - public static $blacklist = [ + public static array $blacklist = [ 2147483648, 2147483647, 1073741824, @@ -48,30 +52,38 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if (\is_string($var) && !\ctype_digit($var)) { - return; + return $v; } if ($var < 0) { - return; + return $v; } if (\in_array($var, self::$blacklist, true)) { - return; + return $v; } $len = \strlen((string) $var); // Guess for anything between March 1973 and November 2286 - if (9 === $len || 10 === $len) { - // If it's an int or string that's this short it probably has no other meaning - // Additionally it's highly unlikely the shortValue will be clipped for length - // If you're writing a plugin that interferes with this, just put your - // parser plugin further down the list so that it gets loaded afterwards. - $o->value->label = 'Timestamp'; - $o->value->hints[] = 'timestamp'; + if ($len < 9 || $len > 10) { + return $v; } + + if (!$v instanceof StringValue && !$v instanceof FixedWidthValue) { + return $v; + } + + if (!$dt = DateTimeImmutable::createFromFormat('U', (string) $var)) { + return $v; + } + + $v->removeRepresentation('contents'); + $v->addRepresentation(new StringRepresentation('Timestamp', $dt->format('c'), null, true)); + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/ToStringPlugin.php b/system/ThirdParty/Kint/Parser/ToStringPlugin.php index 478442bebb73..6fa7bd4e2b33 100644 --- a/system/ThirdParty/Kint/Parser/ToStringPlugin.php +++ b/system/ThirdParty/Kint/Parser/ToStringPlugin.php @@ -27,15 +27,19 @@ namespace Kint\Parser; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\Representation\ValueRepresentation; use ReflectionClass; +use SimpleXMLElement; +use SplFileInfo; +use Throwable; -class ToStringPlugin extends AbstractPlugin +class ToStringPlugin extends AbstractPlugin implements PluginCompleteInterface { - public static $blacklist = [ - 'SimpleXMLElement', - 'SplFileObject', + public static array $blacklist = [ + SimpleXMLElement::class, + SplFileInfo::class, ]; public function getTypes(): array @@ -48,22 +52,37 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { $reflection = new ReflectionClass($var); if (!$reflection->hasMethod('__toString')) { - return; + return $v; } foreach (self::$blacklist as $class) { if ($var instanceof $class) { - return; + return $v; } } - $r = new Representation('toString'); - $r->contents = (string) $var; + try { + $string = (string) $var; + } catch (Throwable $t) { + return $v; + } + + $c = $v->getContext(); + + $base = new BaseContext($c->getName()); + $base->depth = $c->getDepth() + 1; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = '(string) '.$ap; + } + + $string = $this->getParser()->parse($string, $base); + + $v->addRepresentation(new ValueRepresentation('toString', $string)); - $o->addRepresentation($r); + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/TracePlugin.php b/system/ThirdParty/Kint/Parser/TracePlugin.php index a5f47dcfcdf9..533456472609 100644 --- a/system/ThirdParty/Kint/Parser/TracePlugin.php +++ b/system/ThirdParty/Kint/Parser/TracePlugin.php @@ -28,14 +28,23 @@ namespace Kint\Parser; use Kint\Utils; -use Kint\Zval\TraceFrameValue; -use Kint\Zval\TraceValue; -use Kint\Zval\Value; - -class TracePlugin extends AbstractPlugin +use Kint\Value\AbstractValue; +use Kint\Value\ArrayValue; +use Kint\Value\Context\ArrayContext; +use Kint\Value\Representation\ContainerRepresentation; +use Kint\Value\Representation\SourceRepresentation; +use Kint\Value\Representation\ValueRepresentation; +use Kint\Value\TraceFrameValue; +use Kint\Value\TraceValue; +use RuntimeException; + +/** + * @psalm-import-type TraceFrame from TraceFrameValue + */ +class TracePlugin extends AbstractPlugin implements PluginCompleteInterface { - public static $blacklist = ['spl_autoload_call']; - public static $path_blacklist = []; + public static array $blacklist = ['spl_autoload_call']; + public static array $path_blacklist = []; public function getTypes(): array { @@ -47,42 +56,46 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$o->value) { - return; + if (!$v instanceof ArrayValue) { + return $v; } - $trace = $this->parser->getCleanArray($var); + // Shallow copy so we don't have to worry about touching var + $trace = $var; - if (\count($trace) !== \count($o->value->contents) || !Utils::isTrace($trace)) { - return; + if (!Utils::isTrace($trace)) { + return $v; } - $traceobj = new TraceValue(); - $traceobj->transplant($o); - $rep = $traceobj->value; + $pdepth = $this->getParser()->getDepthLimit(); + $c = $v->getContext(); - $old_trace = $rep->contents; + // We need at least 2 levels in order to get $trace[n]['args'] + if ($pdepth && $c->getDepth() + 2 >= $pdepth) { + return $v; + } - Utils::normalizeAliases(self::$blacklist); - $path_blacklist = self::normalizePaths(self::$path_blacklist); + $contents = $v->getContents(); - $rep->contents = []; + self::$blacklist = Utils::normalizeAliases(self::$blacklist); + $path_blacklist = self::normalizePaths(self::$path_blacklist); - foreach ($old_trace as $frame) { - $index = $frame->name; + $frames = []; - if (!isset($trace[$index]['function'])) { - // Something's very very wrong here, but it's probably a plugin's fault + foreach ($contents as $frame) { + if (!$frame instanceof ArrayValue || !$frame->getContext() instanceof ArrayContext) { continue; } - if (Utils::traceFrameIsListed($trace[$index], self::$blacklist)) { + $index = $frame->getContext()->getName(); + + if (!isset($trace[$index]) || Utils::traceFrameIsListed($trace[$index], self::$blacklist)) { continue; } - if (isset($trace[$index]['file']) && ($realfile = \realpath($trace[$index]['file']))) { + if (isset($trace[$index]['file']) && false !== ($realfile = \realpath($trace[$index]['file']))) { foreach ($path_blacklist as $path) { if (0 === \strpos($realfile, $path)) { continue 2; @@ -90,16 +103,39 @@ public function parse(&$var, Value &$o, int $trigger): void } } - $rep->contents[$index] = new TraceFrameValue($frame, $trace[$index]); + $frame = new TraceFrameValue($frame, $trace[$index]); + + if (null !== ($file = $frame->getFile()) && null !== ($line = $frame->getLine())) { + try { + $frame->addRepresentation(new SourceRepresentation($file, $line)); + } catch (RuntimeException $e) { + } + } + + if ($args = $frame->getArgs()) { + $frame->addRepresentation(new ContainerRepresentation('Arguments', $args)); + } + + if ($obj = $frame->getObject()) { + $frame->addRepresentation( + new ValueRepresentation( + 'Callee object ['.$obj->getClassName().']', + $obj, + 'callee_object' + ) + ); + } + + $frames[$index] = $frame; } - \ksort($rep->contents); - $rep->contents = \array_values($rep->contents); + $traceobj = new TraceValue($c, \count($frames), $frames); + + if ($frames) { + $traceobj->addRepresentation(new ContainerRepresentation('Contents', $frames, null, true)); + } - $traceobj->clearRepresentations(); - $traceobj->addRepresentation($rep); - $traceobj->size = \count($rep->contents); - $o = $traceobj; + return $traceobj; } protected static function normalizePaths(array $paths): array diff --git a/system/ThirdParty/Kint/Parser/XmlPlugin.php b/system/ThirdParty/Kint/Parser/XmlPlugin.php index a5a31abd4361..64275a2e9740 100644 --- a/system/ThirdParty/Kint/Parser/XmlPlugin.php +++ b/system/ThirdParty/Kint/Parser/XmlPlugin.php @@ -27,12 +27,19 @@ namespace Kint\Parser; +use Dom\Node; +use Dom\XMLDocument; use DOMDocument; -use Exception; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; - -class XmlPlugin extends AbstractPlugin +use DOMException; +use DOMNode; +use InvalidArgumentException; +use Kint\Value\AbstractValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\Context\ContextInterface; +use Kint\Value\Representation\ValueRepresentation; +use Throwable; + +class XmlPlugin extends AbstractPlugin implements PluginCompleteInterface { /** * Which method to parse the variable with. @@ -41,9 +48,9 @@ class XmlPlugin extends AbstractPlugin * however it's memory usage is very high and it takes longer to parse and * render. Plus it's a pain to work with. So SimpleXML is the default. * - * @var string + * @psalm-var 'SimpleXML'|'DOMDocument'|'XMLDocument' */ - public static $parse_method = 'SimpleXML'; + public static string $parse_method = 'SimpleXML'; public function getTypes(): array { @@ -55,59 +62,54 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if ('access_path); + $c = $v->getContext(); - if (empty($xml)) { - return; - } + $out = \call_user_func([$this, 'xmlTo'.self::$parse_method], $var, $c); - [$xml, $access_path, $name] = $xml; + if (null === $out) { + return $v; + } - $base_obj = new Value(); - $base_obj->depth = $o->depth + 1; - $base_obj->name = $name; - $base_obj->access_path = $access_path; + $out->flags |= AbstractValue::FLAG_GENERATED; - $r = new Representation('XML'); - $r->contents = $this->parser->parse($xml, $base_obj); + $v->addRepresentation(new ValueRepresentation('XML', $out), 0); - $o->addRepresentation($r, 0); + return $v; } - protected static function xmlToSimpleXML(string $var, ?string $parent_path): ?array + /** @psalm-suppress PossiblyUnusedMethod */ + protected function xmlToSimpleXML(string $var, ContextInterface $c): ?AbstractValue { $errors = \libxml_use_internal_errors(true); try { $xml = \simplexml_load_string($var); - } catch (Exception $e) { + if (!(bool) $xml) { + throw new InvalidArgumentException('Bad XML parse in XmlPlugin::xmlToSimpleXML'); + } + } catch (Throwable $t) { return null; } finally { \libxml_use_internal_errors($errors); + \libxml_clear_errors(); } - if (false === $xml) { - return null; - } - - if (null === $parent_path) { - $access_path = null; - } else { - $access_path = 'simplexml_load_string('.$parent_path.')'; + $base = new BaseContext($xml->getName()); + $base->depth = $c->getDepth() + 1; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = 'simplexml_load_string('.$ap.')'; } - $name = $xml->getName(); - - return [$xml, $access_path, $name]; + return $this->getParser()->parse($xml, $base); } /** @@ -115,38 +117,63 @@ protected static function xmlToSimpleXML(string $var, ?string $parent_path): ?ar * * If it errors loading then we wouldn't have gotten this far in the first place. * - * @psalm-param non-empty-string $var The XML string - * - * @param ?string $parent_path The path to the parent, in this case the XML string + * @psalm-suppress PossiblyUnusedMethod * - * @return ?array The root element DOMNode, the access path, and the root element name + * @psalm-param non-empty-string $var */ - protected static function xmlToDOMDocument(string $var, ?string $parent_path): ?array + protected function xmlToDOMDocument(string $var, ContextInterface $c): ?AbstractValue { - // There's no way to check validity in DOMDocument without making errors. For shame! - if (!self::xmlToSimpleXML($var, $parent_path)) { + try { + $xml = new DOMDocument(); + $check = $xml->loadXML($var, LIBXML_NOWARNING | LIBXML_NOERROR); + + if (false === $check) { + throw new InvalidArgumentException('Bad XML parse in XmlPlugin::xmlToDOMDocument'); + } + } catch (Throwable $t) { return null; } - $xml = new DOMDocument(); - $xml->loadXML($var); + $xml = $xml->firstChild; - if ($xml->childNodes->count() > 1) { - $xml = $xml->childNodes; - $access_path = 'childNodes'; - } else { - $xml = $xml->firstChild; - $access_path = 'firstChild'; + /** + * @psalm-var DOMNode $xml + * Psalm bug #11120 + */ + $base = new BaseContext($xml->nodeName); + $base->depth = $c->getDepth() + 1; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = '(function($s){$x = new \\DomDocument(); $x->loadXML($s); return $x;})('.$ap.')->firstChild'; } - if (null === $parent_path) { - $access_path = null; - } else { - $access_path = '(function($s){$x = new \\DomDocument(); $x->loadXML($s); return $x;})('.$parent_path.')->'.$access_path; + return $this->getParser()->parse($xml, $base); + } + + /** @psalm-suppress PossiblyUnusedMethod */ + protected function xmlToXMLDocument(string $var, ContextInterface $c): ?AbstractValue + { + if (!KINT_PHP84) { + return null; // @codeCoverageIgnore } - $name = $xml->nodeName ?? null; + try { + $xml = XMLDocument::createFromString($var, LIBXML_NOWARNING | LIBXML_NOERROR); + } catch (DOMException $e) { + return null; + } + + $xml = $xml->firstChild; + + /** + * @psalm-var Node $xml + * Psalm bug #11120 + */ + $base = new BaseContext($xml->nodeName); + $base->depth = $c->getDepth() + 1; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = '\\Dom\\XMLDocument::createFromString('.$ap.')->firstChild'; + } - return [$xml, $access_path, $name]; + return $this->getParser()->parse($xml, $base); } } diff --git a/system/ThirdParty/Kint/Renderer/AbstractRenderer.php b/system/ThirdParty/Kint/Renderer/AbstractRenderer.php index adec8f071d97..fb4597c2d01f 100644 --- a/system/ThirdParty/Kint/Renderer/AbstractRenderer.php +++ b/system/ThirdParty/Kint/Renderer/AbstractRenderer.php @@ -27,67 +27,38 @@ namespace Kint\Renderer; -use Kint\Zval\InstanceValue; -use Kint\Zval\Value; - -/** - * @psalm-type PluginMap array - * - * @psalm-consistent-constructor - */ -abstract class AbstractRenderer implements RendererInterface +abstract class AbstractRenderer implements ConstructableRendererInterface { - public const SORT_NONE = 0; - public const SORT_VISIBILITY = 1; - public const SORT_FULL = 2; - - protected $call_info = []; - protected $statics = []; - protected $show_trace = true; - - public function setCallInfo(array $info): void - { - if (!isset($info['modifiers']) || !\is_array($info['modifiers'])) { - $info['modifiers'] = []; - } + public static ?string $js_nonce = null; + public static ?string $css_nonce = null; - if (!isset($info['trace']) || !\is_array($info['trace'])) { - $info['trace'] = []; - } + /** @psalm-var ?non-empty-string */ + public static ?string $file_link_format = null; - $this->call_info = [ - 'params' => $info['params'] ?? null, - 'modifiers' => $info['modifiers'], - 'callee' => $info['callee'] ?? null, - 'caller' => $info['caller'] ?? null, - 'trace' => $info['trace'], - ]; - } + protected bool $show_trace = true; + protected ?array $callee = null; + protected array $trace = []; - public function getCallInfo(): array - { - return $this->call_info; - } + protected bool $render_spl_ids = true; - public function setStatics(array $statics): void + public function __construct() { - $this->statics = $statics; - $this->setShowTrace(!empty($statics['display_called_from'])); } - public function getStatics(): array + public function shouldRenderObjectIds(): bool { - return $this->statics; + return $this->render_spl_ids; } - public function setShowTrace(bool $show_trace): void + public function setCallInfo(array $info): void { - $this->show_trace = $show_trace; + $this->callee = $info['callee'] ?? null; + $this->trace = $info['trace'] ?? []; } - public function getShowTrace(): bool + public function setStatics(array $statics): void { - return $this->show_trace; + $this->show_trace = !empty($statics['display_called_from']); } public function filterParserPlugins(array $plugins): array @@ -105,71 +76,12 @@ public function postRender(): string return ''; } - /** - * Returns the first compatible plugin available. - * - * @psalm-param PluginMap $plugins Array of hints to class strings - * @psalm-param string[] $hints Array of object hints - * - * @psalm-return PluginMap Array of hints to class strings filtered and sorted by object hints - */ - public function matchPlugins(array $plugins, array $hints): array - { - $out = []; - - foreach ($hints as $key) { - if (isset($plugins[$key])) { - $out[$key] = $plugins[$key]; - } - } - - return $out; - } - - public static function sortPropertiesFull(Value $a, Value $b): int + public static function getFileLink(string $file, int $line): ?string { - $sort = Value::sortByAccess($a, $b); - if ($sort) { - return $sort; + if (null === self::$file_link_format) { + return null; } - $sort = Value::sortByName($a, $b); - if ($sort) { - return $sort; - } - - return InstanceValue::sortByHierarchy($a->owner_class, $b->owner_class); - } - - /** - * Sorts an array of Value. - * - * @param Value[] $contents Object properties to sort - * - * @return Value[] - */ - public static function sortProperties(array $contents, int $sort): array - { - switch ($sort) { - case self::SORT_VISIBILITY: - // Containers to quickly stable sort by type - $containers = [ - Value::ACCESS_PUBLIC => [], - Value::ACCESS_PROTECTED => [], - Value::ACCESS_PRIVATE => [], - Value::ACCESS_NONE => [], - ]; - - foreach ($contents as $item) { - $containers[$item->access][] = $item; - } - - return \call_user_func_array('array_merge', $containers); - case self::SORT_FULL: - \usort($contents, [self::class, 'sortPropertiesFull']); - // no break - default: - return $contents; - } + return \str_replace(['%f', '%l'], [$file, $line], self::$file_link_format); } } diff --git a/system/ThirdParty/Kint/Zval/InstanceValue.php b/system/ThirdParty/Kint/Renderer/AssetRendererTrait.php similarity index 55% rename from system/ThirdParty/Kint/Zval/InstanceValue.php rename to system/ThirdParty/Kint/Renderer/AssetRendererTrait.php index 323733474825..11bf9547c4b4 100644 --- a/system/ThirdParty/Kint/Zval/InstanceValue.php +++ b/system/ThirdParty/Kint/Renderer/AssetRendererTrait.php @@ -25,50 +25,40 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace Kint\Zval; +namespace Kint\Renderer; -class InstanceValue extends Value +trait AssetRendererTrait { - public $type = 'object'; - public $classname; - public $spl_object_hash; - public $spl_object_id = null; - public $filename; - public $startline; - public $hints = ['object']; + public static ?string $theme = null; - public function getType(): ?string - { - return $this->classname; - } + /** @psalm-var array{js?:string, css?:array} */ + private static array $assetCache = []; - public function transplant(Value $old): void + /** @psalm-api */ + public static function renderJs(): string { - parent::transplant($old); - - if ($old instanceof self) { - $this->classname = $old->classname; - $this->spl_object_hash = $old->spl_object_hash; - $this->spl_object_id = $old->spl_object_id; - $this->filename = $old->filename; - $this->startline = $old->startline; + if (!isset(self::$assetCache['js'])) { + self::$assetCache['js'] = \file_get_contents(KINT_DIR.'/resources/compiled/main.js'); } + + return self::$assetCache['js']; } - /** - * @psalm-param class-string $a - * @psalm-param class-string $b - */ - public static function sortByHierarchy(string $a, string $b): int + /** @psalm-api */ + public static function renderCss(): ?string { - if (\is_subclass_of($a, $b)) { - return -1; + if (!isset(self::$theme)) { + return null; } - if (\is_subclass_of($b, $a)) { - return 1; + if (!isset(self::$assetCache['css'][self::$theme])) { + if (\file_exists(KINT_DIR.'/resources/compiled/'.self::$theme)) { + self::$assetCache['css'][self::$theme] = \file_get_contents(KINT_DIR.'/resources/compiled/'.self::$theme); + } else { + self::$assetCache['css'][self::$theme] = \file_get_contents(self::$theme); + } } - return 0; + return self::$assetCache['css'][self::$theme]; } } diff --git a/system/ThirdParty/Kint/Renderer/CliRenderer.php b/system/ThirdParty/Kint/Renderer/CliRenderer.php index c2ea103b6644..6efdbe83203d 100644 --- a/system/ThirdParty/Kint/Renderer/CliRenderer.php +++ b/system/ThirdParty/Kint/Renderer/CliRenderer.php @@ -27,7 +27,7 @@ namespace Kint\Renderer; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; use Throwable; class CliRenderer extends TextRenderer @@ -35,52 +35,46 @@ class CliRenderer extends TextRenderer /** * @var bool enable colors */ - public static $cli_colors = true; - - /** - * Forces utf8 output on windows. - * - * @var bool - */ - public static $force_utf8 = false; + public static bool $cli_colors = true; /** * Detects the terminal width on startup. - * - * @var bool */ - public static $detect_width = true; + public static bool $detect_width = true; /** * The minimum width to detect terminal size as. * * Less than this is ignored and falls back to default width. - * - * @var int */ - public static $min_terminal_width = 40; + public static int $min_terminal_width = 40; + + /** + * Forces utf8 output on windows. + */ + public static bool $force_utf8 = false; /** * Which stream to check for VT100 support on windows. * * uses STDOUT by default if it's defined * - * @var ?resource + * @psalm-var ?resource */ public static $windows_stream = null; - protected static $terminal_width = null; + protected static ?int $terminal_width = null; - protected $windows_output = false; + protected bool $windows_output = false; - protected $colors = false; + protected bool $colors = false; public function __construct() { parent::__construct(); if (!self::$force_utf8 && KINT_WIN) { - if (!KINT_PHP72 || !\function_exists('sapi_windows_vt100_support')) { + if (!\function_exists('sapi_windows_vt100_support')) { $this->windows_output = true; } else { $stream = self::$windows_stream; @@ -97,16 +91,23 @@ public function __construct() } } - if (!self::$terminal_width) { - if (!KINT_WIN && self::$detect_width) { + if (null === self::$terminal_width) { + if (self::$detect_width) { try { - self::$terminal_width = (int) \exec('tput cols'); + $tput = KINT_WIN ? \exec('tput cols 2>nul') : \exec('tput cols 2>/dev/null'); + if ((bool) $tput) { + /** + * @psalm-suppress InvalidCast + * Psalm bug #11080 + */ + self::$terminal_width = (int) $tput; + } } catch (Throwable $t) { self::$terminal_width = self::$default_width; } } - if (self::$terminal_width < self::$min_terminal_width) { + if (!isset(self::$terminal_width) || self::$terminal_width < self::$min_terminal_width) { self::$terminal_width = self::$default_width; } } @@ -143,13 +144,13 @@ public function colorTitle(string $string): string return "\x1b[36m".\str_replace("\n", "\x1b[0m\n\x1b[36m", $string)."\x1b[0m"; } - public function renderTitle(Value $o): string + public function renderTitle(AbstractValue $v): string { if ($this->windows_output) { - return $this->utf8ToWindows(parent::renderTitle($o)); + return $this->utf8ToWindows(parent::renderTitle($v)); } - return parent::renderTitle($o); + return parent::renderTitle($v); } public function preRender(): string diff --git a/system/ThirdParty/Kint/Renderer/Text/DepthLimitPlugin.php b/system/ThirdParty/Kint/Renderer/ConstructableRendererInterface.php similarity index 85% rename from system/ThirdParty/Kint/Renderer/Text/DepthLimitPlugin.php rename to system/ThirdParty/Kint/Renderer/ConstructableRendererInterface.php index cbb151882044..686f37a645f6 100644 --- a/system/ThirdParty/Kint/Renderer/Text/DepthLimitPlugin.php +++ b/system/ThirdParty/Kint/Renderer/ConstructableRendererInterface.php @@ -25,14 +25,9 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace Kint\Renderer\Text; +namespace Kint\Renderer; -use Kint\Zval\Value; - -class DepthLimitPlugin extends AbstractPlugin +interface ConstructableRendererInterface extends RendererInterface { - public function render(Value $o): string - { - return $this->renderLockedHeader($o, 'DEPTH LIMIT'); - } + public function __construct(); } diff --git a/system/ThirdParty/Kint/Renderer/PlainRenderer.php b/system/ThirdParty/Kint/Renderer/PlainRenderer.php index 7210f55f3072..ac949e5b6169 100644 --- a/system/ThirdParty/Kint/Renderer/PlainRenderer.php +++ b/system/ThirdParty/Kint/Renderer/PlainRenderer.php @@ -27,46 +27,38 @@ namespace Kint\Renderer; -use Kint\Kint; -use Kint\Zval\BlobValue; -use Kint\Zval\Value; +use Kint\Utils; +use Kint\Value\AbstractValue; class PlainRenderer extends TextRenderer { - public static $pre_render_sources = [ + use AssetRendererTrait; + + public static array $pre_render_sources = [ 'script' => [ - ['Kint\\Renderer\\PlainRenderer', 'renderJs'], - ['Kint\\Renderer\\Text\\MicrotimePlugin', 'renderJs'], + [self::class, 'renderJs'], ], 'style' => [ - ['Kint\\Renderer\\PlainRenderer', 'renderCss'], + [self::class, 'renderCss'], ], 'raw' => [], ]; - /** - * Path to the CSS file to load by default. - * - * @var string - */ - public static $theme = 'plain.css'; - /** * Output htmlentities instead of utf8. - * - * @var bool */ - public static $disable_utf8 = false; + public static bool $disable_utf8 = false; - public static $needs_pre_render = true; + public static bool $needs_pre_render = true; - public static $always_pre_render = false; + public static bool $always_pre_render = false; - protected $force_pre_render = false; + protected bool $force_pre_render = false; public function __construct() { parent::__construct(); + self::$theme ??= 'plain.css'; $this->setForcePreRender(self::$always_pre_render); } @@ -74,7 +66,7 @@ public function setCallInfo(array $info): void { parent::setCallInfo($info); - if (\in_array('@', $this->call_info['modifiers'], true)) { + if (\in_array('@', $info['modifiers'], true)) { $this->setForcePreRender(true); } } @@ -118,13 +110,13 @@ public function colorTitle(string $string): string return ''.$string.''; } - public function renderTitle(Value $o): string + public function renderTitle(AbstractValue $v): string { if (self::$disable_utf8) { - return $this->utf8ToHtmlentity(parent::renderTitle($o)); + return $this->utf8ToHtmlentity(parent::renderTitle($v)); } - return parent::renderTitle($o); + return parent::renderTitle($v); } public function preRender(): string @@ -144,10 +136,18 @@ public function preRender(): string switch ($type) { case 'script': - $output .= ''; + $output .= '