From a614110a0f67360b6d4a49aab25bd5fcfa9bfe52 Mon Sep 17 00:00:00 2001 From: Jonathan Vollebregt Date: Mon, 10 Apr 2017 20:27:07 +0200 Subject: [PATCH] Kint_SourceParser: Less scary preg_match, more proper parsing Why such a complicated solution to a fixed problem? The previous preg_match based option had some big bugs. For example, if a query was called through call_user_func_array() the preg match wouldn't match it and would instead grab the first call to that method *before* the call, even if it was thousands of lines earlier. The more robust parsing also allows us to more correctly guess which function call is the one in question on a line by counting the number of parameters it was called with. Lastly, this allows us to provide access paths even when we don't know what the parameter is called. Currently the arguments will be numeric (Since they won't be mistaken for a real variable) and start from $0 and go up. Like most nice things, there's a performance penalty for this, but since it's only run once per call it's not exponential like changes to the parser and its plugins. The tiny extra overhead per call is absolutely worth it --- init.php | 2 + src/Kint.php | 262 ++++------------- src/SourceParser.php | 464 ++++++++++++++++++++++++++++++ tests/Kint_SourceParserTest.php | 486 ++++++++++++++++++++++++++++++++ tests/basic.php | 9 +- 5 files changed, 1018 insertions(+), 205 deletions(-) create mode 100644 src/SourceParser.php create mode 100644 tests/Kint_SourceParserTest.php diff --git a/init.php b/init.php index a73e5f977..a40788f07 100644 --- a/init.php +++ b/init.php @@ -19,12 +19,14 @@ define('KINT_PHP524', (version_compare(PHP_VERSION, '5.2.4') >= 0)); define('KINT_PHP525', (version_compare(PHP_VERSION, '5.2.5') >= 0)); define('KINT_PHP53', (version_compare(PHP_VERSION, '5.3') >= 0)); +define('KINT_PHP56', (version_compare(PHP_VERSION, '5.6') >= 0)); define('KINT_PHP70', (version_compare(PHP_VERSION, '7.0') >= 0)); define('KINT_PHP72', (version_compare(PHP_VERSION, '7.2') >= 0)); // Only preload classes if no autoloader specified if (!class_exists('Kint', true)) { require_once KINT_DIR.'/src/Kint.php'; + require_once KINT_DIR.'/src/SourceParser.php'; // Data require_once KINT_DIR.'/src/Object.php'; diff --git a/src/Kint.php b/src/Kint.php index 3d00d91aa..c17c6bb03 100644 --- a/src/Kint.php +++ b/src/Kint.php @@ -229,11 +229,13 @@ public static function dump($data = null) } $stash = self::settings(); + $num_args = func_num_args(); - list($names, $parameters, $modifiers, $callee, $caller, $mini_trace) = self::getCalleeInfo( + list($params, $modifiers, $callee, $caller, $minitrace) = self::getCalleeInfo( defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) - : debug_backtrace() + : debug_backtrace(), + $num_args ); // set mode for current run @@ -271,12 +273,12 @@ public static function dump($data = null) } $renderer = new $renderer(array( - 'param_shortnames' => $names, - 'param_names' => $parameters, + 'num_args' => $num_args, + 'params' => $params, 'modifiers' => $modifiers, 'callee' => $callee, 'caller' => $caller, - 'minitrace' => $mini_trace, + 'minitrace' => $minitrace, 'settings' => self::settings(), 'stash' => $stash, )); @@ -298,7 +300,7 @@ public static function dump($data = null) } // Kint::dump(1) shorthand - if (($names == array('1') || $names === array(null)) && func_num_args() === 1 && $data === 1) { + if ((!isset($params[0]['name']) || $params[0]['name'] == '1') && $num_args === 1 && $data === 1) { if (KINT_PHP525) { $trace = debug_backtrace(true); } else { @@ -324,32 +326,23 @@ public static function dump($data = null) $output .= call_user_func(array($renderer, 'render'), new Kint_Object_Nothing()); } - $blacklist = array('null', 'true', 'false', 'array(...)', 'array()', '"..."', 'b"..."', '[...]', '[]', '(...)', '()'); + static $blacklist = array('null', 'true', 'false', 'array(...)', 'array()', '"..."', 'b"..."', '[...]', '[]', '(...)', '()'); foreach ($data as $i => $argument) { - if (isset($parameters[$i])) { - $access_path = $parameters[$i]; - - if (!isset($names[$i])) { - $access_path = '('.$access_path.')'; - } elseif (!in_array(str_replace("'", '"', strtolower($names[$i])), $blacklist, true)) { - // Strips spaces from around [], (), \, -> and :: - $name = preg_replace('/(\s+(?=(\(|\[|->|::|\\\\))|(?<=(\)|\]|\\\\))\s+|(?<=(->|::))\s+)/', '', $names[$i]); - - // Once the spaces are gone we can do a (relatively) simple check to - // see if it contains anything that might turn it into an expression - if (!preg_match('/^@?(\\\\[a-z_])?(->|::|[0-9a-z_]\\\\[a-z_]|[\[\]\.\(\)\$A-Z0-9_]+)+$/i', $name)) { - $access_path = '('.$access_path.')'; - } - } + if (!isset($params[$i]['name']) || is_numeric($params[$i]['name']) || in_array(str_replace("'", '"', strtolower($params[$i]['name'])), $blacklist, true)) { + $name = null; } else { - $access_path = '(...)'; + $name = $params[$i]['name']; } - if (!isset($names[$i]) || is_numeric($names[$i]) || in_array(str_replace("'", '"', strtolower($names[$i])), $blacklist, true)) { - $name = null; + if (isset($params[$i]['path'])) { + $access_path = $params[$i]['path']; + + if (!empty($params[$i]['expression'])) { + $access_path = '('.$access_path.')'; + } } else { - $name = $names[$i]; + $access_path = '$'.$i; } $output .= call_user_func( @@ -430,9 +423,9 @@ public static function getIdeLink($file, $line) * * @param array $trace * - * @return array($names, $parameters, $modifier, $callee, $caller, $miniTrace) + * @return array($params, $modifiers, $callee, $caller, $miniTrace) */ - private static function getCalleeInfo($trace) + private static function getCalleeInfo($trace, $num_params) { $miniTrace = array(); @@ -467,196 +460,65 @@ private static function getCalleeInfo($trace) $miniTrace = array_values($miniTrace); if (!isset($callee['file'], $callee['line']) || !is_readable($callee['file'])) { - return array(null, null, array(), $callee, $caller, $miniTrace); + return array(null, array(), $callee, $caller, $miniTrace); } // open the file and read it up to the position where the function call expression ended - $file = fopen($callee['file'], 'r'); - $line = 0; - $source = ''; - while (($row = fgets($file)) !== false) { - if (++$line > $callee['line']) { - break; - } - $source .= $row; - } - fclose($file); - $source = self::removeAllButCode($source); - if (empty($callee['class'])) { - $codePattern = $callee['function']; - } elseif ($callee['type'] === '::') { - $codePattern = $callee['class']."\x07*".$callee['type']."\x07*".$callee['function']; + $callfunc = $callee['function']; } else { - $codePattern = ".*\x07*".$callee['type']."\x07*".$callee['function']; + $callfunc = array($callee['class'], $callee['function']); } - // TODO: if more than one call in one line - not possible to determine variable names - // get the position of the last call to the function - preg_match_all(" - / - # beginning of statement - [\x07{(] - - # search for modifiers (group 1) - ([-+!@~]*)? - - # spaces - \x07* - - # check if output is assigned to a variable (group 2) - ( - \\$[a-z0-9_]+ # variable - \x07*\\.?=\x07* # assignment - )? - - # possibly a namespace symbol - \\\\? - - # spaces again - \x07* - - # main call to Kint - ({$codePattern}) - - # spaces everywhere - \x07* - - # find the character where kint's opening bracket resides (group 3) - (\\() - - /ix", - $source, - $matches, - PREG_OFFSET_CAPTURE + $calls = Kint_SourceParser::getFunctionCalls( + file_get_contents($callee['file']), + $callee['line'], + $callfunc ); - $modifiers = end($matches[1]); - $assignment = end($matches[2]); - $callToKint = end($matches[3]); - $bracket = end($matches[4]); - - if (empty($callToKint)) { - // if a wrapper is misconfigured, don't display the whole file as variable name - return array(array(), array(), str_split((string) $modifiers), $callee, $caller, $miniTrace); - } - - $modifiers = str_split((string) $modifiers[0]); - if ($assignment[1] !== -1) { - $modifiers[] = '@'; - } - - $paramsRaw = $paramsString = preg_replace("[\x07+]", ' ', substr($source, $bracket[1] + 1)); - // we now have a string like this: - // ); - - // remove everything in brackets and quotes, we don't need nested statements nor literal strings which would - // only complicate separating individual arguments - $c = strlen($paramsString); - $inString = $escaped = $openedBracket = $closingBracket = false; - $i = 0; - $paramStart = 0; - $inBrackets = 0; - $openedBrackets = array(); - $parameters = array(); - - while ($i < $c) { - $letter = $paramsString[$i]; - - if (!$inString) { - if ($letter === '\'' || $letter === '"') { - $inString = $letter; - } elseif ($letter === '(' || $letter === '[') { - ++$inBrackets; - $openedBrackets[] = $openedBracket = $letter; - $closingBracket = $openedBracket === '(' ? ')' : ']'; - } elseif ($inBrackets && $letter === $closingBracket) { - --$inBrackets; - array_pop($openedBrackets); - $openedBracket = end($openedBrackets); - $closingBracket = $openedBracket === '(' ? ')' : ']'; - } elseif (!$inBrackets && $letter === ')') { - $paramsString = substr($paramsString, 0, $i); - break; - } elseif (!$inBrackets && $letter === ',') { - $parameters[] = trim(substr($paramsRaw, $paramStart, $i - $paramStart)); - $paramStart = $i + 1; - } - } elseif ($letter === $inString && !$escaped) { - $inString = false; - } + $return = array(null, array(), $callee, $caller, $miniTrace); + + foreach ($calls as $call) { + $is_unpack = false; + + // Handle argument unpacking as a last resort + if (KINT_PHP56) { + foreach ($call['parameters'] as $i => &$param) { + if (strpos($param['name'], '...') === 0) { + if ($i === count($call['parameters']) - 1) { + for ($j = 1; $j + $i < $num_params; ++$j) { + $call['parameters'][] = array( + 'name' => 'array_values('.substr($param['name'], 3).')['.$j.']', + 'path' => 'array_values('.substr($param['path'], 3).')['.$j.']', + 'expression' => false, + ); + } + + $param['name'] = 'reset('.substr($param['name'], 3).')'; + $param['path'] = 'reset('.substr($param['path'], 3).')'; + $param['expression'] = false; + } else { + $call['parameters'] = array_slice($call['parameters'], 0, $i); + } - // replace whatever was inside quotes or brackets with untypeable characters, we don't - // need that info. We'll later replace the whole string with '...' - if ($inBrackets > 0) { - if ($inBrackets > 1 || $letter !== $openedBracket) { - $paramsString[$i] = "\x07"; - } - } - if ($inString) { - if ($letter !== $inString || $escaped) { - $paramsString[$i] = "\x07"; + $is_unpack = true; + break; + } } } - $escaped = !$escaped && ($letter === '\\'); - ++$i; - } - - $final_arg = trim(substr($paramsRaw, $paramStart, $i - $paramStart)); - if ($final_arg) { - $parameters[] = $final_arg; - } - - // by now we have an un-nested arguments list, lets make it to an array for processing further - $arguments = explode(',', preg_replace("[\x07+]", '...', $paramsString)); - - // test each argument whether it was passed literary or was it an expression or a variable name - $names = array(); - foreach ($arguments as $argument) { - $names[] = trim($argument); - } - - return array($names, $parameters, $modifiers, $callee, $caller, $miniTrace); - } - - /** - * removes comments and zaps whitespace & true, T_INLINE_HTML => true, T_DOC_COMMENT => true, - ); - $whiteSpaceTokens = array( - T_WHITESPACE => true, T_CLOSE_TAG => true, - T_OPEN_TAG => true, T_OPEN_TAG_WITH_ECHO => true, - ); - - $cleanedSource = ''; - foreach (token_get_all($source) as $token) { - if (is_array($token)) { - if (isset($commentTokens[$token[0]])) { - continue; - } - - if (isset($whiteSpaceTokens[$token[0]])) { - $token = "\x07"; + if ($is_unpack || count($call['parameters']) === $num_params) { + if ($return[0] === null) { + $return = array($call['parameters'], $call['modifiers'], $callee, $caller, $miniTrace); } else { - $token = $token[1]; + // If we have multiple calls on the same line with the same amount of arguments, + // we can't be sure which it is so just return null and let them figure it out + return array(null, array(), $callee, $caller, $miniTrace); } - } elseif ($token === ';') { - $token = "\x07"; } - - $cleanedSource .= $token; } - return $cleanedSource; + return $return; } public static function composerGetExtras($key = 'kint') diff --git a/src/SourceParser.php b/src/SourceParser.php new file mode 100644 index 000000000..e14aae605 --- /dev/null +++ b/src/SourceParser.php @@ -0,0 +1,464 @@ + 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 $operator = array( + 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_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_NEW => 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, + '!' => true, + '%' => true, + '&' => true, + '*' => true, + '+' => true, + '-' => true, + '.' => true, + '/' => true, + ':' => true, + '<' => true, + '=' => true, + '>' => true, + '?' => true, + '^' => true, + '|' => true, + '~' => true, + ); + + private static $strip = array( + '(' => true, + ')' => true, + '[' => true, + ']' => true, + '{' => true, + '}' => true, + T_OBJECT_OPERATOR => true, + T_DOUBLE_COLON => true, + ); + + public static function getFunctionCalls($source, $line, $function) + { + $tokens = token_get_all($source); + $cursor = 1; + $function_calls = array(); + + if (is_array($function)) { + $class = explode('\\', $function[0]); + $function = array(strtolower(end($class)), strtolower($function[1])); + } else { + $function = strtolower($function); + } + + // Loop through tokens + foreach ($tokens as $index => $token) { + if (is_array($token)) { + // Count newlines for line number instead of using + // $token[2] since it's not available until 5.2.2 + // Also note that 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) { + break; + } + + // If it's the function we're looking for + if ($token[0] === T_STRING && $cursor <= $line && self::tokenIsFunction($tokens, $index, $function)) { + $inner_cursor = $cursor; + $params = self::getFunctionParameters($tokens, $index, $inner_cursor); + + if ($params && $inner_cursor >= $line) { + $modifiers = self::getFunctionModifiers($tokens, $index); + $parameters = array(); + + foreach ($params as $param) { + $name = self::tokensFormatted($param['short']); + $expression = false; + foreach ($name as $token) { + if (self::tokenIsOperator($token)) { + $expression = true; + break; + } + } + + $parameters[] = array( + 'name' => self::tokensToString($name), + 'path' => self::tokensToString(self::tokensTrim($param['full'])), + 'expression' => $expression, + ); + } + + $function_calls[] = array( + 'parameters' => $parameters, + 'modifiers' => $modifiers, + ); + } + } + } + } + + return $function_calls; + } + + private static function tokenIsFunction(array $tokens, $index, $function) + { + if (self::nextRealToken($tokens, $index) !== '(') { + return false; + } + + if (!is_array($tokens[$index])) { + return false; + } + + $last = self::realTokenIndex($tokens, $index, -1); + + if (is_string($function)) { + if ($tokens[$index][0] !== T_STRING || strtolower($tokens[$index][1]) !== $function) { + return false; + } + + if ($last && in_array($tokens[$last][0], array(T_DOUBLE_COLON, T_OBJECT_OPERATOR))) { + return false; + } + } elseif (is_array($function)) { + if ($tokens[$index][0] !== T_STRING || strtolower($tokens[$index][1]) !== $function[1]) { + return false; + } + + if (!$last || $tokens[$last][0] !== T_DOUBLE_COLON) { + return false; + } + + $class = self::realTokenIndex($tokens, $last, -1); + + if (!$class || $tokens[$class][0] !== T_STRING || strtolower($tokens[$class][1]) !== $function[0]) { + return false; + } + } + + return true; + } + + private static function getFunctionModifiers(array $tokens, $index) + { + static $modifiers = array( + '!', + '@', + '~', + '+', + '-', + ); + + $ret = array(); + --$index; + + while (isset($tokens[$index])) { + if (self::tokenIsIgnored($tokens[$index])) { + --$index; + continue; + } elseif (is_array($tokens[$index]) && empty($ret) && in_array($tokens[$index][0], array(T_DOUBLE_COLON, T_NS_SEPARATOR, T_STRING))) { + --$index; + continue; + } elseif (is_string($tokens[$index]) && in_array($tokens[$index], $modifiers)) { + $ret[] = $tokens[$index]; + --$index; + continue; + } else { + break; + } + } + + return $ret; + } + + private static function getFunctionParameters(array $tokens, $index, &$cursor) + { + static $up = array( + '(' => true, + '[' => true, + '{' => true, + T_CURLY_OPEN => true, + T_DOLLAR_OPEN_CURLY_BRACES => true, + T_STRING_VARNAME => true, + ); + static $down = array( + ')' => true, + ']' => true, + '}' => true, + ); + + $depth = 0; // The depth respective to the function call + $offset = 1; // The offset from the function call + $instring = false; // Whether we're in a string or not + $parameters = array(); // All our collected parameters + $shortparam = array(); // The short version of the parameter + $param_start = 1; // The distance to the start of the parameter + + // Loop through the following tokens until the function call ends + while (isset($tokens[$index + $offset])) { + $token = $tokens[$index + $offset]; + + // Ensure that the $cursor is correct and that + // $token is either a T_ constant or a string + if (is_array($token)) { + $cursor += substr_count($token[1], "\n"); + } + + if (!self::tokenIsIgnored($token) && !isset($down[$token[0]])) { + $realtokens = true; + } + + // If it's a token that makes us to up a level, increase the depth + if (isset($up[$token[0]])) { + // If this is the first paren set the start of the param to just after it + if ($depth === 0) { + $param_start = $offset + 1; + } elseif ($depth === 1) { + $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 ($depth === 1) { + if ($realtokens) { + $shortparam[] = '...'; + } + $shortparam[] = $token; + } + } elseif ($token[0] === '"') { + // 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 ($depth === 1) { + $shortparam[] = '...'; + } + } else { + ++$depth; + } + + $instring = !$instring; + + $shortparam[] = '"'; + } elseif ($depth === 1) { + if ($token[0] === ',') { + $parameters[] = array( + 'full' => array_slice($tokens, $index + $param_start, $offset - $param_start), + 'short' => $shortparam, + ); + $shortparam = array(); + $param_start = $offset + 1; + } elseif ($token[0] === T_CONSTANT_ENCAPSED_STRING && strlen($token[1]) > 2) { + $shortparam[] = $token[1][0].'...'.$token[1][0]; + } else { + $shortparam[] = $token; + } + } + + // Depth has dropped to 0 (So we've hit the closing paren) + if ($depth <= 0) { + $parameters[] = array( + 'full' => array_slice($tokens, $index + $param_start, $offset - $param_start), + 'short' => $shortparam, + ); + + return $parameters; + } + + ++$offset; + } + + return null; + } + + private static function nextRealToken(array $tokens, $index) + { + return $tokens[self::realTokenIndex($tokens, $index, 1)]; + } + + private static function lastRealToken(array $tokens, $index) + { + return $tokens[self::realTokenIndex($tokens, $index, -1)]; + } + + private static function realTokenIndex(array $tokens, $index, $direction) + { + $index += $direction; + + while (isset($tokens[$index])) { + if (!self::tokenIsIgnored($tokens[$index])) { + return $index; + } + + $index += $direction; + } + + return null; + } + + private static function tokenIs($token, $compare) + { + if (is_array($token)) { + return isset($compare[$token[0]]); + } else { + return isset($compare[$token]); + } + } + + private static function tokenIsIgnored($token) + { + return self::tokenIs($token, self::$ignore); + } + + private static function tokenIsOperator($token) + { + if (KINT_PHP56) { + self::$operator[T_POW] = true; + self::$operator[T_POW_EQUAL] = true; + } + + if (KINT_PHP70) { + self::$operator[T_SPACESHIP] = true; + } + + return self::tokenIs($token, self::$operator); + } + + private static function tokenIsStrip($token) + { + if (KINT_PHP53) { + self::$strip[T_NS_SEPARATOR] = true; + } + + return self::tokenIs($token, self::$strip); + } + + private static function tokensToString(array $tokens) + { + $out = ''; + + foreach ($tokens as $token) { + if (is_string($token)) { + $out .= $token; + } elseif (is_array($token)) { + $out .= $token[1]; + } + } + + return $out; + } + + private static function tokensTrim(array $tokens) + { + foreach ($tokens as $index => $token) { + if (self::tokenIsIgnored($token)) { + unset($tokens[$index]); + } else { + break; + } + } + + $tokens = array_reverse($tokens); + + foreach ($tokens as $index => $token) { + if (self::tokenIsIgnored($token)) { + unset($tokens[$index]); + } else { + break; + } + } + + return array_reverse($tokens); + } + + private static function tokensFormatted(array $tokens) + { + $space = false; + + $tokens = self::tokensTrim($tokens); + + $output = array(); + + foreach ($tokens as $index => $token) { + if (self::tokenIsIgnored($token)) { + if ($space) { + continue; + } + + $last = self::lastRealToken($tokens, $index); + $next = self::nextRealToken($tokens, $index); + + if (self::tokenIsStrip($last) && !self::tokenIsOperator($next)) { + continue; + } elseif (self::tokenIsStrip($next) && !self::tokenIsOperator($last)) { + continue; + } + + $token = ' '; + $space = true; + } else { + $space = false; + } + + $output[] = $token; + } + + return $output; + } +} diff --git a/tests/Kint_SourceParserTest.php b/tests/Kint_SourceParserTest.php new file mode 100644 index 000000000..56f833d8a --- /dev/null +++ b/tests/Kint_SourceParserTest.php @@ -0,0 +1,486 @@ + 3, + 'function' => 'Test', + 'skipto' => 0, + 'result' => array( + array( + 'modifiers' => array('~', '-', '+', '@', '!'), + 'parameters' => array( + array( + 'path' => '$wat', + 'name' => '$wat', + 'expression' => false, + ), + array( + 'path' => '$woot[$wat] + 4', + 'name' => '$woot[...] + 4', + 'expression' => true, + ), + ), + ), + ), + ); + + $data['static method'] = array( + ' 3, + 'function' => array('namespace\\subspace\\c', 'method'), + 'skipto' => 0, + 'result' => array( + array( + 'modifiers' => array('!'), + 'parameters' => array( + array( + 'path' => '[]', + 'name' => '[]', + 'expression' => false, + ), + array( + 'path' => '[ ]', + 'name' => '[]', + 'expression' => false, + ), + array( + 'path' => '[ 1 ]', + 'name' => '[...]', + 'expression' => false, + ), + ), + ), + ), + ); + + $data['multiple on one line'] = array( + ' 3, + 'function' => 'test', + 'skipto' => 0, + 'result' => array( + array( + 'modifiers' => array('!'), + 'parameters' => array( + array( + 'path' => '$val', + 'name' => '$val', + 'expression' => false, + ), + ), + ), + array( + 'modifiers' => array('@'), + 'parameters' => array( + array( + 'path' => '[ ]', + 'name' => '[]', + 'expression' => false, + ), + array( + 'path' => '$_SERVER["REMOTE_ADDR"]', + 'name' => '$_SERVER[...]', + 'expression' => false, + ), + ), + ), + ), + ); + + $data['one on multiple lines start'] = array( + ' 3, + 'function' => 'test', + 'skipto' => 0, + 'result' => array( + array( + 'modifiers' => array('!'), + 'parameters' => array( + array( + 'path' => '$val', + 'name' => '$val', + 'expression' => false, + ), + array( + 'path' => '$_SERVER[$val]', + 'name' => '$_SERVER[...]', + 'expression' => false, + ), + ), + ), + ), + ); + + $data['one on multiple lines end'] = $data['one on multiple lines start']; + $data['one on multiple lines end']['line'] = 7; + + $data['one on multiple lines mid'] = $data['one on multiple lines start']; + $data['one on multiple lines mid']['line'] = 5; + + $data['nested calls'] = array( + ' 4, + 'function' => 'test', + 'skipto' => 0, + 'result' => array( + array( + 'modifiers' => array('!'), + 'parameters' => array( + array( + 'path' => '@test($val)', + 'name' => '@test(...)', + 'expression' => false, + ), + array( + 'path' => '$_SERVER[$val]', + 'name' => '$_SERVER[...]', + 'expression' => false, + ), + ), + ), + array( + 'modifiers' => array('@'), + 'parameters' => array( + array( + 'path' => '$val', + 'name' => '$val', + 'expression' => false, + ), + ), + ), + ), + ); + + $data['nested calls, single matching line'] = $data['nested calls']; + $data['nested calls, single matching line']['line'] = 5; + unset($data['nested calls, single matching line']['result'][1]); + + $data['multiple line params'] = array( + 'comments 4, + 'function' => 'test', + 'skipto' => 0, + 'result' => array( + array( + 'modifiers' => array(), + 'parameters' => array( + array( + 'path' => '$a /* mixed */ + /** in */ $b ?>comments '$a + $b + $c', + 'expression' => true, + ), + ), + ), + ), + ); + + $data['space stripping'] = array( + ' 3, + 'function' => 'test', + 'skipto' => 0, + 'result' => array( + array( + 'modifiers' => array(), + 'parameters' => array( + array( + 'path' => '$var [ "key" ] + /* test */ $var2 +$var3', + 'name' => '$var[...] + $var2 +$var3', + 'expression' => true, + ), + ), + ), + ), + ); + + $data['expressions'] = array( + ' 10, + 'function' => 'd', + 'skipto' => 0, + 'result' => array( + array( + 'modifiers' => array(), + 'parameters' => array( + array( + 'path' => 'true?$_SERVER:array()', + 'name' => 'true?$_SERVER:array()', + 'expression' => true, + ), + array( + 'path' => '$x=1', + 'name' => '$x=1', + 'expression' => true, + ), + array( + 'path' => '$x+1', + 'name' => '$x+1', + 'expression' => true, + ), + array( + 'path' => '$x==1', + 'name' => '$x==1', + 'expression' => true, + ), + array( + 'path' => '$x-1', + 'name' => '$x-1', + 'expression' => true, + ), + array( + 'path' => '$x*1', + 'name' => '$x*1', + 'expression' => true, + ), + array( + 'path' => '$x/1', + 'name' => '$x/1', + 'expression' => true, + ), + array( + 'path' => '$x%1', + 'name' => '$x%1', + 'expression' => true, + ), + array( + 'path' => '$x++', + 'name' => '$x++', + 'expression' => true, + ), + array( + 'path' => '$x--', + 'name' => '$x--', + 'expression' => true, + ), + array( + 'path' => '$x**4', + 'name' => '$x**4', + 'expression' => true, + ), + array( + 'path' => '~$x', + 'name' => '~$x', + 'expression' => true, + ), + array( + 'path' => '$x instanceof bltest', + 'name' => '$x instanceof bltest', + 'expression' => true, + ), + array( + 'path' => '!$x', + 'name' => '!$x', + 'expression' => true, + ), + array( + 'path' => '$x%1', + 'name' => '$x%1', + 'expression' => true, + ), + array( + 'path' => '$_SERVER["HTTP_HOST"]', + 'name' => '$_SERVER[...]', + 'expression' => false, + ), + array( + 'path' => '$_SERVER[ "HTTP_HOST" ]', + 'name' => '$_SERVER[...]', + 'expression' => false, + ), + array( + 'path' => '$_SERVER [ "HTTP_HOST" ]', + 'name' => '$_SERVER[...]', + 'expression' => false, + ), + array( + 'path' => '[] + []', + 'name' => '[] + []', + 'expression' => true, + ), + array( + 'path' => 'new DateTime()', + 'name' => 'new DateTime()', + 'expression' => true, + ), + array( + 'path' => 'clone $db', + 'name' => 'clone $db', + 'expression' => true, + ), + array( + 'path' => 'array()', + 'name' => 'array()', + 'expression' => false, + ), + array( + 'path' => 'array( )', + 'name' => 'array()', + 'expression' => false, + ), + array( + 'path' => '[]', + 'name' => '[]', + 'expression' => false, + ), + array( + 'path' => '[ ]', + 'name' => '[]', + 'expression' => false, + ), + array( + 'path' => '((((((("woot")))))))', + 'name' => '(...)', + 'expression' => false, + ), + array( + 'path' => 'true', + 'name' => 'true', + 'expression' => false, + ), + array( + 'path' => 'TRUE', + 'name' => 'TRUE', + 'expression' => false, + ), + array( + 'path' => 'test::TEST', + 'name' => 'test::TEST', + 'expression' => false, + ), + array( + 'path' => '\test::TEST', + 'name' => '\test::TEST', + 'expression' => false, + ), + array( + 'path' => 'test :: TEST', + 'name' => 'test::TEST', + 'expression' => false, + ), + array( + 'path' => '\test :: TEST', + 'name' => '\test::TEST', + 'expression' => false, + ), + ), + ), + ), + ); + + if (KINT_PHP56) { + $data['arg expansion'] = array( + ' 3, + 'function' => 'test', + 'skipto' => 0, + 'result' => array( + array( + 'modifiers' => array(), + 'parameters' => array( + array( + 'path' => '$args', + 'name' => '$args', + 'expression' => false, + ), + array( + 'path' => '...$args', + 'name' => '...$args', + 'expression' => false, + ), + ), + ), + ), + ); + } + + return $data; + } + + /** + * @dataProvider sourceProvider + */ + public function testGetFunctionCalls($source, $line, $function, $skipto, $result) + { + $output = Kint_SourceParser::getFunctionCalls($source, $line, $function, $skipto); + + $this->assertCount(count($result), $output); + + foreach ($result as $index => $function) { + $this->assertEquals($function, $output[$index]); + } + } +} diff --git a/tests/basic.php b/tests/basic.php index 83347ac73..8fc2b2bcf 100644 --- a/tests/basic.php +++ b/tests/basic.php @@ -40,20 +40,19 @@ function error() $expected = '0.+?integer.+?1234.+?1.+?stdClass.+?1.+?public.+?abc.+?string.+?3.+?def.+?2.+?double.+?1234\\.5678.+?3.+?string.+?43.+?Good news everyone! I\'ve got some bad news!.+?4.+?null.+?5.+?(&|&)array'; $expected = '/'.$expected.'.+?6.+?'.$expected.'.+RECURSION/si'; +Kint::$return = true; + echo 'CLI'.PHP_EOL; -preg_match($expected, @d($testdata)) || exit(1); +preg_match($expected, d($testdata)) || exit(1); -Kint::$return = true; Kint::$cli_detection = false; echo 'RICH'.PHP_EOL; preg_match($expected, d($testdata)) || exit(1); echo 'PLAIN'.PHP_EOL; preg_match($expected, s($testdata)) || exit(1); - -Kint::$enabled_mode = Kint::MODE_TEXT; echo 'TEXT'.PHP_EOL; -preg_match($expected, d($testdata)) || exit(1); +preg_match($expected, ~~d($testdata)) || exit(1); if ($error) { echo 'Errors occurred'.PHP_EOL;