Skip to content

Commit

Permalink
Update Backtrace utility
Browse files Browse the repository at this point in the history
  • Loading branch information
bkdotcom committed Aug 31, 2023
1 parent 28d6e3c commit b4bbc9a
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 323 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ $monolog->critical('all your base are belong to them');
![Build Status](https://img.shields.io/github/actions/workflow/status/bkdotcom/PHPDebugConsole/phpunit.yml.svg?logo=github)
[![Codacy Score](https://img.shields.io/codacy/grade/e950849edfd9463b993386080d39875e/master.svg?logo=codacy)](https://app.codacy.com/gh/bkdotcom/PHPDebugConsole/dashboard)
[![Maintainability](https://img.shields.io/codeclimate/maintainability/bkdotcom/PHPDebugConsole.svg?logo=codeclimate)](https://codeclimate.com/github/bkdotcom/PHPDebugConsole)
[![Coverage](https://img.shields.io/codeclimate/coverage-letter/bkdotcom/PHPDebugConsole?logo=codeclimate)](https://codeclimate.com/github/bkdotcom/PHPDebugConsole)
[![Coverage](https://img.shields.io/codeclimate/coverage-letter/bkdotcom/PHPDebugConsole.svg?logo=codeclimate)](https://codeclimate.com/github/bkdotcom/PHPDebugConsole)

## Changelog

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
}
},
"replace": {
"bdk/backtrace": "2.1.1",
"bdk/backtrace": "2.2",
"bdk/curl-http-message": "1.0",
"bdk/errorhandler": "3.3",
"bdk/http-message": "1.0",
Expand Down
224 changes: 112 additions & 112 deletions src/Backtrace/Backtrace.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @package Backtrace
* @author Brad Kent <bkfake-github@yahoo.com>
* @license http://opensource.org/licenses/MIT MIT
* @copyright 2020-2022 Brad Kent
* @copyright 2020-2023 Brad Kent
* @version v2.1
* @link http://www.github.com/bkdotcom/Backtrace
*/
Expand All @@ -13,6 +13,8 @@

use bdk\Backtrace\Normalizer;
use bdk\Backtrace\SkipInternal;
use Exception;
use ParseError;

/**
* Utility for getting backtrace
Expand All @@ -28,7 +30,7 @@ class Backtrace
const INCL_OBJECT = 2;

/**
* add a new namespace or classname to be used to determine when to
* Add a new namespace or classname to be used to determine when to
* stop iterrating over the backtrace when determining calling info
*
* @param array|string $classes classname(s)
Expand All @@ -45,8 +47,9 @@ public static function addInternalClass($classes, $level = 0)
/**
* Helper method to get backtrace
*
* Uses passed exception, debug_backtrace, or xdebug_get_function_stack
*
* Utilizes `xdebug_get_function_stack()` (if available) to get backtrace in shutdown phase
* When called internally, internal frames are removed
*
* @param int|null $options bitmask of options
* @param int $limit limit the number of stack frames returned.
Expand All @@ -56,16 +59,24 @@ public static function addInternalClass($classes, $level = 0)
*/
public static function get($options = 0, $limit = 0, $exception = null)
{
$options = $options ?: 0;
$backtrace = static::getBacktrace($options, $limit, $exception);
if (empty($backtrace)) {
return array();
$limit = $limit ?: null;
if ($exception) {
$backtrace = self::getExceptionTrace($exception);
return \array_slice($backtrace, 0, $limit);
}
// don't incl args passed to trace()
$backtrace[0]['args'] = array();
$options = self::translateOptions($options);
$backtrace = \debug_backtrace($options, $limit ? $limit + 2 : 0);
if (\array_key_exists('file', \end($backtrace)) === false) {
// We're in shutdown
$backtrace = static::xdebugGetFunctionStack() ?: array();
$backtrace = \array_reverse($backtrace);
}
$backtrace = Normalizer::normalize($backtrace);
$backtrace = SkipInternal::removeInternalFrames($backtrace);
// keep the calling file & line, but toss the called function (what initiated trace)
unset($backtrace[0]['function']);
return $backtrace;
unset($backtrace[\count($backtrace) - 1]['function']); // remove "{main}"
return \array_slice($backtrace, 0, $limit);
}

/**
Expand Down Expand Up @@ -95,7 +106,9 @@ public static function getCallerInfo($offset = 0, $options = 0)
*/
$phpOptions = static::translateOptions($options | self::INCL_OBJECT);
$backtrace = \debug_backtrace($phpOptions, 28);
$backtrace = Normalizer::normalize($backtrace);
$index = SkipInternal::getFirstIndex($backtrace, $offset);
$index = \max($index, 1); // insure we're >= 1
$return = static::callerInfoBuild(\array_slice($backtrace, $index));
if (!($options & self::INCL_OBJECT)) {
unset($return['object']);
Expand All @@ -104,9 +117,8 @@ public static function getCallerInfo($offset = 0, $options = 0)
}

/**
* Add lines surrounding frame line to each frame
* Add context (code snippet) to each frame
*
* Adds a `context` value to each backtrace frame
* context is an array of `lineNumber => line`
*
* @param array $backtrace backtrace frames
Expand Down Expand Up @@ -136,7 +148,7 @@ public static function addContext($backtrace, $length = 19)
* Returns array of lineNumber => line
*
* @param string $file filepath
* @param int $start line to start on (1-indexed; 1 = line; 1 = first line)
* @param int $start line to start on (1-indexed; 1 = first line)
* 0 also = first line
* @param int $length number of lines to return
*
Expand All @@ -150,7 +162,7 @@ public static function getFileLines($file, $start = 1, $length = null)
return false;
}
$lines = \array_merge(array(null), \file($file));
if ($start === 0) {
if ($start <= 0) {
$start = 1;
}
if ($start > 1 || $length) {
Expand Down Expand Up @@ -180,6 +192,46 @@ public static function isXdebugFuncStackAvail()
return true;
}

/**
* Wrapper for xdebug_get_function_stack
* accounts for bug 1529 (may report incorrect file)
*
* xdebug.collect_params ini must be set prior to running code to be backtraced for params (args) to be collected
*
* @return array|false
*
* @see https://bugs.xdebug.org/view.php?id=695
* @see https://bugs.xdebug.org/view.php?id=1529
* @see https://xdebug.org/docs/all_settings#xdebug.collect_params
*/
public static function xdebugGetFunctionStack()
{
if (static::isXdebugFuncStackAvail() === false) {
return false;
}
$stack = \xdebug_get_function_stack();
$xdebugVer = \phpversion('xdebug');
if (\version_compare($xdebugVer, '2.6.0', '<')) {
$stack = static::xdebugFix($stack);
}
$error = \error_get_last();
if ($error !== null && $error['type'] & (E_ERROR | E_PARSE | E_COMPILE_ERROR | E_CORE_ERROR)) {
// xdebug_get_function_stack doesn't include the frame that triggered the error!
$errorFileLine = array(
'file' => $error['file'],
'line' => $error['line'],
);
$lastFrame = \end($stack);
if (\array_intersect_assoc($errorFileLine, $lastFrame) !== $errorFileLine) {
\array_push($stack, $errorFileLine);
}
}
return \array_map(static function ($frame) {
\ksort($frame);
return $frame;
}, $stack);
}

/**
* Build callerInfo array from given backtrace segment
*
Expand All @@ -190,31 +242,39 @@ public static function isXdebugFuncStackAvail()
private static function callerInfoBuild($backtrace)
{
$return = array(
'args' => array(),
'class' => null, // where the method is defined
'classCalled' => null, // parent::method()... this will be the parent class
'classContext' => null, // child->method()
'evalLine' => null,
'file' => null,
'function' => null,
'line' => null,
'type' => null,
);
$numFrames = \count($backtrace);
$iFileLine = 0;
$iFunc = 1;
if (isset($backtrace[$iFunc])) {
$return = \array_merge($return, $backtrace[$iFunc]);
$return = \array_merge(
$return,
$backtrace[$iFunc],
\preg_match('/^(?<class>\S+)(?<type>::|->)(?<method>\S+)$/', $backtrace[$iFunc]['function'], $matches)
? array(
'class' => $matches['class'],
'function' => $matches['method'],
'type' => $matches['type'],
)
: array()
);
$return['classCalled'] = $return['class'];
}
if ($return['type'] === '->') {
$return['classContext'] = \get_class($backtrace[$iFunc]['object']);
$return = self::callerInfoClassCalled($return);
}
if (isset($backtrace[$iFileLine])) {
$return['file'] = $backtrace[$iFileLine]['file'];
$return['line'] = $backtrace[$iFileLine]['line'];
} elseif (isset($backtrace[$numFrames - 1])) {
$return['file'] = $backtrace[$numFrames - 1]['file'];
$return['line'] = 0;
}
if ($return['type'] === '->') {
$return['classContext'] = \get_class($backtrace[$iFunc]['object']);
$return = self::callerInfoClassCalled($return);
}
return $return;
}
Expand All @@ -232,92 +292,40 @@ private static function callerInfoClassCalled($info)
// class : classname of parent (or where method defined)
// object : scope / context
$info['classCalled'] = $info['classContext'];
if ($info['function'] === '{closure}') {
return $info;
}
$classDeclared = null;
if ($info['classContext'] !== $info['class']) {
if ($info['function'] === '{closure}') {
return $info;
}
$reflector = new \ReflectionMethod($info['classContext'], $info['function']);
$classDeclared = $reflector->getDeclaringClass()->getName();
if ($classDeclared === $info['classContext']) {
// method is (re)declared in classContext, yet that's not what's being executed
// we must have called parent::method()
$info['classCalled'] = $info['class'];
}
}
if ($classDeclared === $info['classContext']) {
// method is (re)declared in classContext, yet that's not what's being executed
// we must have called parent::method()
$info['classCalled'] = $info['class'];
}
return $info;
}

/**
* Get backtrace from either passed exception,
* debug_backtrace or xdebug_get_function_stack
* Get trace from exception
*
* @param int $options options bitmask
* @param int $limit limit the number of stack frames returned.
* @param \Exception|\Throwable $exception (optional) Exception from which to get backtrace
* @param Exception|Throwable $exception Exception instance
*
* @return array|false
* @return array
*/
private static function getBacktrace($options = 0, $limit = 0, $exception = null)
private static function getExceptionTrace($exception)
{
if ($exception instanceof \ParseError) {
if ($exception instanceof ParseError) {
return array();
}
$limit = $limit ?: null;
if ($exception) {
$backtrace = $exception->getTrace();
\array_unshift($backtrace, array(
'file' => $exception->getFile(),
'line' => $exception->getLine(),
));
$backtrace = Normalizer::normalize($backtrace);
return \array_slice($backtrace, 0, $limit);
}
$options = static::translateOptions($options);
$backtrace = \debug_backtrace($options, $limit ? $limit + 2 : 0);
if (\array_key_exists('file', \end($backtrace)) === true) {
// We're NOT in shutdown
$backtrace = Normalizer::normalize($backtrace);
$backtrace = SkipInternal::removeInternalFrames($backtrace);
return \array_slice($backtrace, 0, $limit);
}
/*
We appear to be in shutdown - use xdebug
*/
return static::getBacktraceXdebug($limit);
}

/**
* Get backtrace via xdebug
*
* @param int $limit limit the number of stack frames returned.
*
* @return array|false
*/
private static function getBacktraceXdebug($limit)
{
$backtrace = static::xdebugGetFunctionStack();
if ($backtrace === false) {
return false;
}
$backtrace = \array_reverse($backtrace);
$backtrace = Normalizer::normalize($backtrace);
$backtrace = SkipInternal::removeInternalFrames($backtrace);
$backtrace = \array_slice($backtrace, 0, $limit ?: null);
$error = \error_get_last();
if ($error !== null && $error['type'] & (E_ERROR | E_PARSE | E_COMPILE_ERROR | E_CORE_ERROR)) {
// xdebug_get_function_stack doesn't include the frame that triggered the error!
$errorFileLine = array(
'file' => $error['file'],
'line' => $error['line'],
);
if (\array_intersect_assoc($errorFileLine, $backtrace[0]) !== $errorFileLine) {
\array_unshift($backtrace, $errorFileLine);
}
}
\end($backtrace);
$key = \key($backtrace);
unset($backtrace[$key]['function']); // remove "{main}"
return $backtrace;
$backtrace = $exception->getTrace();
\array_unshift($backtrace, array(
'file' => $exception->getFile(),
'line' => $exception->getLine(),
));
return Normalizer::normalize($backtrace);
}

/**
Expand All @@ -329,6 +337,7 @@ private static function getBacktraceXdebug($limit)
*/
private static function translateOptions($options)
{
$options = $options ?: 0;
$phpOptions = DEBUG_BACKTRACE_IGNORE_ARGS;
if ($options & self::INCL_ARGS) {
$phpOptions &= ~DEBUG_BACKTRACE_IGNORE_ARGS;
Expand All @@ -340,33 +349,24 @@ private static function translateOptions($options)
}

/**
* wrapper for xdebug_get_function_stack
* accounts for bug 1529 (may report incorrect file)
* Fix xdebug bugs
*
* xdebug.collect_params ini must be set prior to running code to be backtraced for params (args) to be collected
* https://bugs.xdebug.org/view.php?id=695 - doesn't set the call type key
* https://bugs.xdebug.org/view.php?id=1529 - __get : wrong file
*
* @return array|false
* @see https://bugs.xdebug.org/view.php?id=695
* @see https://bugs.xdebug.org/view.php?id=1529
* @see https://xdebug.org/docs/all_settings#xdebug.collect_params
* @param array $stack xdebug stack
*
* @return array
*/
private static function xdebugGetFunctionStack()
private static function xdebugFix(array $stack)
{
if (static::isXdebugFuncStackAvail() === false) {
return false;
}
$stack = \xdebug_get_function_stack();
$xdebugVer = \phpversion('xdebug');
if (\version_compare($xdebugVer, '2.6.0', '>=')) {
return $stack;
}
$count = \count($stack);
for ($i = 0; $i < $count; $i++) {
$frame = \array_merge(array(
'function' => null,
), $stack[$i]);
if (!isset($frame['type']) && isset($frame['class'])) {
// XDebug pre 2.1.1 doesn't set the call type key http://bugs.xdebug.org/view.php?id=695
// XDebug pre 2.1.1 doesn't set the call type key https://bugs.xdebug.org/view.php?id=695
$stack[$i]['type'] = 'static';
}
if ($frame['function'] !== '__get') {
Expand Down
Loading

0 comments on commit b4bbc9a

Please sign in to comment.