660 lines
21 KiB
PHP
660 lines
21 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2013 Jonathan Vollebregt (jnvsor@gmail.com), Rokas Šleinius (raveren@gmail.com)
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
* this software and associated documentation files (the "Software"), to deal in
|
|
* the Software without restriction, including without limitation the rights to
|
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
* subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
namespace Kint;
|
|
|
|
/**
|
|
* @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 array $ignore = [
|
|
T_CLOSE_TAG => true,
|
|
T_COMMENT => true,
|
|
T_DOC_COMMENT => true,
|
|
T_INLINE_HTML => true,
|
|
T_OPEN_TAG => true,
|
|
T_OPEN_TAG_WITH_ECHO => true,
|
|
T_WHITESPACE => true,
|
|
];
|
|
|
|
/**
|
|
* Things we need to do specially for operator tokens:
|
|
* - Refuse to strip spaces around them
|
|
* - Wrap the access path in parentheses if there
|
|
* are any of these in the final short parameter.
|
|
*/
|
|
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_CLONE => true,
|
|
T_CONCAT_EQUAL => true,
|
|
T_DEC => true,
|
|
T_DIV_EQUAL => true,
|
|
T_DOUBLE_CAST => true,
|
|
T_FUNCTION => true,
|
|
T_INC => true,
|
|
T_INCLUDE => true,
|
|
T_INCLUDE_ONCE => true,
|
|
T_INSTANCEOF => true,
|
|
T_INT_CAST => true,
|
|
T_IS_EQUAL => true,
|
|
T_IS_GREATER_OR_EQUAL => true,
|
|
T_IS_IDENTICAL => true,
|
|
T_IS_NOT_EQUAL => true,
|
|
T_IS_NOT_IDENTICAL => true,
|
|
T_IS_SMALLER_OR_EQUAL => true,
|
|
T_LOGICAL_AND => true,
|
|
T_LOGICAL_OR => true,
|
|
T_LOGICAL_XOR => true,
|
|
T_MINUS_EQUAL => true,
|
|
T_MOD_EQUAL => true,
|
|
T_MUL_EQUAL => true,
|
|
T_OBJECT_CAST => true,
|
|
T_OR_EQUAL => true,
|
|
T_PLUS_EQUAL => true,
|
|
T_REQUIRE => true,
|
|
T_REQUIRE_ONCE => true,
|
|
T_SL => true,
|
|
T_SL_EQUAL => true,
|
|
T_SR => true,
|
|
T_SR_EQUAL => true,
|
|
T_STRING_CAST => true,
|
|
T_UNSET_CAST => true,
|
|
T_XOR_EQUAL => true,
|
|
T_POW => true,
|
|
T_POW_EQUAL => true,
|
|
T_SPACESHIP => true,
|
|
T_DOUBLE_ARROW => true,
|
|
T_FN => true,
|
|
T_COALESCE_EQUAL => true,
|
|
'!' => true,
|
|
'%' => true,
|
|
'&' => true,
|
|
'*' => true,
|
|
'+' => true,
|
|
'-' => true,
|
|
'.' => true,
|
|
'/' => true,
|
|
':' => true,
|
|
'<' => true,
|
|
'=' => true,
|
|
'>' => true,
|
|
'?' => true,
|
|
'^' => true,
|
|
'|' => true,
|
|
'~' => true,
|
|
];
|
|
|
|
private static array $preserve_spaces = [
|
|
T_CLASS => true,
|
|
T_NEW => true,
|
|
];
|
|
|
|
private static array $strip = [
|
|
'(' => true,
|
|
')' => true,
|
|
'[' => true,
|
|
']' => true,
|
|
'{' => true,
|
|
'}' => true,
|
|
T_OBJECT_OPERATOR => true,
|
|
T_DOUBLE_COLON => true,
|
|
T_NS_SEPARATOR => true,
|
|
];
|
|
|
|
private static array $classcalls = [
|
|
T_DOUBLE_COLON => true,
|
|
T_OBJECT_OPERATOR => true,
|
|
];
|
|
|
|
private static array $namespace = [
|
|
T_STRING => true,
|
|
];
|
|
|
|
/**
|
|
* @psalm-param callable-array|callable-string $function
|
|
*
|
|
* @psalm-return list<array{parameters: list<CallParameter>, modifiers: list<PhpToken>}>
|
|
*
|
|
* @return array List of matching calls on the relevant line
|
|
*/
|
|
public static function getFunctionCalls(string $source, int $line, $function): array
|
|
{
|
|
static $up = [
|
|
'(' => true,
|
|
'[' => true,
|
|
'{' => true,
|
|
T_CURLY_OPEN => true,
|
|
T_DOLLAR_OPEN_CURLY_BRACES => true,
|
|
];
|
|
static $down = [
|
|
')' => true,
|
|
']' => true,
|
|
'}' => true,
|
|
];
|
|
static $modifiers = [
|
|
'!' => true,
|
|
'@' => true,
|
|
'~' => true,
|
|
'+' => true,
|
|
'-' => true,
|
|
];
|
|
static $identifier = [
|
|
T_DOUBLE_COLON => true,
|
|
T_STRING => true,
|
|
T_NS_SEPARATOR => true,
|
|
];
|
|
|
|
if (KINT_PHP80) {
|
|
$up[T_ATTRIBUTE] = true;
|
|
self::$operator[T_MATCH] = true;
|
|
self::$strip[T_NULLSAFE_OBJECT_OPERATOR] = true;
|
|
self::$classcalls[T_NULLSAFE_OBJECT_OPERATOR] = true;
|
|
self::$namespace[T_NAME_FULLY_QUALIFIED] = true;
|
|
self::$namespace[T_NAME_QUALIFIED] = true;
|
|
self::$namespace[T_NAME_RELATIVE] = true;
|
|
$identifier[T_NAME_FULLY_QUALIFIED] = true;
|
|
$identifier[T_NAME_QUALIFIED] = true;
|
|
$identifier[T_NAME_RELATIVE] = true;
|
|
}
|
|
|
|
if (!KINT_PHP84) {
|
|
self::$operator[T_NEW] = true; // @codeCoverageIgnore
|
|
}
|
|
|
|
/** @psalm-var list<PhpToken> */
|
|
$tokens = \token_get_all($source);
|
|
$function_calls = [];
|
|
|
|
// Performance optimization preventing backwards loops
|
|
/** @psalm-var array<PhpToken|null> */
|
|
$prev_tokens = [null, null, null];
|
|
|
|
if (\is_array($function)) {
|
|
$class = \explode('\\', $function[0]);
|
|
$class = \strtolower(\end($class));
|
|
$function = \strtolower($function[1]);
|
|
} else {
|
|
$class = null;
|
|
/**
|
|
* @psalm-suppress RedundantFunctionCallGivenDocblockType
|
|
* Psalm bug #11075
|
|
*/
|
|
$function = \strtolower($function);
|
|
}
|
|
|
|
// Loop through tokens
|
|
foreach ($tokens as $index => $token) {
|
|
if (!\is_array($token)) {
|
|
continue;
|
|
}
|
|
|
|
if ($token[2] > $line) {
|
|
break;
|
|
}
|
|
|
|
// Store the last real tokens for later
|
|
if (isset(self::$ignore[$token[0]])) {
|
|
continue;
|
|
}
|
|
|
|
$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;
|
|
}
|
|
|
|
$ns = \explode('\\', \strtolower($token[1]));
|
|
|
|
if (\end($ns) !== $function) {
|
|
continue;
|
|
}
|
|
|
|
// Check if it's a function call
|
|
$nextReal = self::realTokenIndex($tokens, $index);
|
|
if ('(' !== ($tokens[$nextReal] ?? null)) {
|
|
continue;
|
|
}
|
|
|
|
// Check if it matches the signature
|
|
if (null === $class) {
|
|
if (null !== $prev_tokens[1] && isset(self::$classcalls[$prev_tokens[1][0]])) {
|
|
continue;
|
|
}
|
|
} else {
|
|
if (null === $prev_tokens[1] || T_DOUBLE_COLON !== $prev_tokens[1][0]) {
|
|
continue;
|
|
}
|
|
|
|
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 bug #746 (wontfix)
|
|
*/
|
|
$ns = \explode('\\', \strtolower($prev_tokens[0][1]));
|
|
|
|
if (\end($ns) !== $class) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$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
|
|
$realtokens = false; // Whether the current scope contains anything meaningful or not
|
|
$paramrealtokens = false; // Whether the current parameter contains anything meaningful
|
|
$params = []; // All our collected parameters
|
|
$shortparam = []; // The short version of the parameter
|
|
$param_start = $offset; // The distance to the start of the parameter
|
|
|
|
// Loop through the following tokens until the function call ends
|
|
while (isset($tokens[$offset])) {
|
|
$token = $tokens[$offset];
|
|
|
|
if (\is_array($token)) {
|
|
$last_line = $token[2];
|
|
}
|
|
|
|
if (!isset(self::$ignore[$token[0]]) && !isset($down[$token[0]])) {
|
|
$paramrealtokens = $realtokens = true;
|
|
}
|
|
|
|
// If it's a token that makes us to up a level, increase the depth
|
|
if (isset($up[$token[0]])) {
|
|
if (1 === $depth) {
|
|
$shortparam[] = $token;
|
|
$realtokens = false;
|
|
}
|
|
|
|
++$depth;
|
|
} elseif (isset($down[$token[0]])) {
|
|
--$depth;
|
|
|
|
// If this brings us down to the parameter level, and we've had
|
|
// real tokens since going up, fill the $shortparam with an ellipsis
|
|
if (1 === $depth) {
|
|
if ($realtokens) {
|
|
$shortparam[] = '...';
|
|
}
|
|
$shortparam[] = $token;
|
|
}
|
|
} 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) {
|
|
--$depth;
|
|
if (1 === $depth) {
|
|
$shortparam[] = '...';
|
|
}
|
|
} else {
|
|
++$depth;
|
|
}
|
|
|
|
$instring = !$instring;
|
|
|
|
$shortparam[] = $token;
|
|
} elseif (1 === $depth) {
|
|
if (',' === $token[0]) {
|
|
$params[] = [
|
|
'full' => \array_slice($tokens, $param_start, $offset - $param_start),
|
|
'short' => $shortparam,
|
|
];
|
|
$shortparam = [];
|
|
$paramrealtokens = false;
|
|
$param_start = $offset + 1;
|
|
} 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;
|
|
}
|
|
}
|
|
|
|
// Depth has dropped to 0 (So we've hit the closing paren)
|
|
if ($depth <= 0) {
|
|
if ($paramrealtokens) {
|
|
$params[] = [
|
|
'full' => \array_slice($tokens, $param_start, $offset - $param_start),
|
|
'short' => $shortparam,
|
|
];
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
++$offset;
|
|
}
|
|
|
|
// If we're not passed (or at) the line at the end
|
|
// of the function call, we're too early so skip it
|
|
// 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) {
|
|
$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;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 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
|
|
if (KINT_PHP81 && 1 === \count($formatted_parameters) && '...' === \reset($formatted_parameters)['path']) {
|
|
continue;
|
|
}
|
|
|
|
// Get the modifiers
|
|
--$index;
|
|
|
|
while (isset($tokens[$index])) {
|
|
if (!isset(self::$ignore[$tokens[$index][0]]) && !isset($identifier[$tokens[$index][0]])) {
|
|
break;
|
|
}
|
|
|
|
--$index;
|
|
}
|
|
|
|
$mods = [];
|
|
|
|
while (isset($tokens[$index])) {
|
|
if (isset(self::$ignore[$tokens[$index][0]])) {
|
|
--$index;
|
|
continue;
|
|
}
|
|
|
|
if (isset($modifiers[$tokens[$index][0]])) {
|
|
$mods[] = $tokens[$index];
|
|
--$index;
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
$function_calls[] = [
|
|
'parameters' => $formatted_parameters,
|
|
'modifiers' => $mods,
|
|
];
|
|
}
|
|
|
|
return $function_calls;
|
|
}
|
|
|
|
private static function realTokenIndex(array $tokens, int $index): ?int
|
|
{
|
|
++$index;
|
|
|
|
while (isset($tokens[$index])) {
|
|
if (!isset(self::$ignore[$tokens[$index][0]])) {
|
|
return $index;
|
|
}
|
|
|
|
++$index;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* We need a separate method to check if tokens are operators because we
|
|
* occasionally add "..." to short parameter versions. If we simply check
|
|
* for `$token[0]` then "..." will incorrectly match the "." operator.
|
|
*
|
|
* @psalm-param PhpToken $token The token to check
|
|
*/
|
|
private static function tokenIsOperator($token): bool
|
|
{
|
|
return '...' !== $token && isset(self::$operator[$token[0]]);
|
|
}
|
|
|
|
/**
|
|
* @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 = '';
|
|
|
|
foreach ($tokens as $token) {
|
|
if (\is_string($token)) {
|
|
$out .= $token;
|
|
} else {
|
|
$out .= $token[1];
|
|
}
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
private static function tokensTrim(array $tokens): array
|
|
{
|
|
foreach ($tokens as $index => $token) {
|
|
if (isset(self::$ignore[$token[0]])) {
|
|
unset($tokens[$index]);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
$tokens = \array_reverse($tokens);
|
|
|
|
foreach ($tokens as $index => $token) {
|
|
if (isset(self::$ignore[$token[0]])) {
|
|
unset($tokens[$index]);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return \array_reverse($tokens);
|
|
}
|
|
|
|
private static function tokensFormatted(array $tokens): array
|
|
{
|
|
$tokens = self::tokensTrim($tokens);
|
|
|
|
$space = false;
|
|
$attribute = false;
|
|
// Keep space between "strip" symbols for different behavior for matches or closures
|
|
// Normally we want to strip spaces between strip tokens: $x{...}[...]
|
|
// However with closures and matches we don't: function (...) {...}
|
|
$ignorestrip = false;
|
|
$output = [];
|
|
$last = null;
|
|
|
|
if (T_FUNCTION === $tokens[0][0] ||
|
|
T_FN === $tokens[0][0] ||
|
|
(KINT_PHP80 && T_MATCH === $tokens[0][0])
|
|
) {
|
|
$ignorestrip = true;
|
|
}
|
|
|
|
foreach ($tokens as $index => $token) {
|
|
if (isset(self::$ignore[$token[0]])) {
|
|
if ($space) {
|
|
continue;
|
|
}
|
|
|
|
$next = self::realTokenIndex($tokens, $index);
|
|
if (null === $next) {
|
|
// This should be impossible, since we always call tokensTrim first
|
|
break; // @codeCoverageIgnore
|
|
}
|
|
$next = $tokens[$next];
|
|
|
|
/**
|
|
* @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::tokenPreserveWhitespace($next)) {
|
|
continue;
|
|
}
|
|
|
|
if (!$ignorestrip && isset(self::$strip[$next[0]]) && !self::tokenPreserveWhitespace($last)) {
|
|
continue;
|
|
}
|
|
|
|
$token[1] = ' ';
|
|
$space = true;
|
|
} else {
|
|
if (KINT_PHP80 && null !== $last && T_ATTRIBUTE === $last[0]) {
|
|
$attribute = true;
|
|
}
|
|
|
|
$space = false;
|
|
$last = $token;
|
|
}
|
|
|
|
$output[] = $token;
|
|
}
|
|
|
|
return $output;
|
|
}
|
|
}
|