diff --git a/composer.json b/composer.json
index 9be9a9415..09c813c44 100644
--- a/composer.json
+++ b/composer.json
@@ -43,7 +43,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.10-dev"
+ "dev-master": "3.0-dev"
}
}
}
diff --git a/examples/xdebug.php b/examples/xdebug.php
new file mode 100644
index 000000000..2282d9203
--- /dev/null
+++ b/examples/xdebug.php
@@ -0,0 +1,66 @@
+
+
+
+
Tracy: exception demo
+
+second();
+ }
+
+
+ public function second()
+ {
+ self::third([1, 2, 3]);
+ }
+
+
+ public static function third($arg5)
+ {
+ //require __DIR__ . '/assets/E_COMPILE_WARNING-1.php';
+ //require __DIR__ . '/assets/E_COMPILE_ERROR.php';
+// trigger_error('jo', E_USER_ERROR);
+// dump(new Exception);
+// dumpe(xdebug_get_function_stack( [ 'local_vars' => true, 'params_as_values' => true ] ));
+ try {
+ throw new Exception('Original');
+ } catch (Exception $e) {
+ throw new Exception('The my exception', 123, $e);
+ }
+ $a++;
+ }
+}
+
+
+
+function demo($a, $b)
+{
+ $demo = new DemoClass;
+ $demo->first($a, $b);
+}
+
+
+if (Debugger::$productionMode) {
+ echo 'For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.
';
+}
+
+demo(10, 'any string');
diff --git a/readme.md b/readme.md
index f726dc45c..6148b11fa 100644
--- a/readme.md
+++ b/readme.md
@@ -50,8 +50,9 @@ Alternatively, you can download the whole package or [tracy.phar](https://github
| Tracy | compatible with PHP | compatible with browsers
|-----------|---------------|----------
-| Tracy 2.10| PHP 8.0 – 8.3 | Chrome 64+, Firefox 69+, Safari 15.4+ and iOS Safari 15.4+
-| Tracy 2.9 | PHP 7.2 – 8.2 | Chrome 64+, Firefox 69+, Safari 13.1+ and iOS Safari 13.4+
+| Tracy 3.0 | PHP 8.0 – 8.3 | Chrome 112+, Firefox 117+, Safari 16.5+
+| Tracy 2.10| PHP 8.0 – 8.3 | Chrome 64+, Firefox 69+, Safari 15.4+
+| Tracy 2.9 | PHP 7.2 – 8.2 | Chrome 64+, Firefox 69+, Safari 13.1+
Usage
diff --git a/src/Bridges/Nette/Bridge.php b/src/Bridges/Nette/Bridge.php
index ccb2f1c1c..41d68033a 100644
--- a/src/Bridges/Nette/Bridge.php
+++ b/src/Bridges/Nette/Bridge.php
@@ -9,7 +9,6 @@
namespace Tracy\Bridges\Nette;
-use Latte;
use Nette;
use Tracy;
use Tracy\BlueScreen;
@@ -17,87 +16,18 @@
/**
- * Bridge for NEON & Latte.
+ * Bridge for NEON.
*/
class Bridge
{
public static function initialize(): void
{
$blueScreen = Tracy\Debugger::getBlueScreen();
- if (!class_exists(Latte\Bridges\Tracy\BlueScreenPanel::class)) {
- $blueScreen->addPanel([self::class, 'renderLatteError']);
- $blueScreen->addAction([self::class, 'renderLatteUnknownMacro']);
- $blueScreen->addFileGenerator(fn(string $file) => substr($file, -6) === '.latte'
- ? "{block content}\n\$END\$"
- : null);
- Tracy\Debugger::addSourceMapper([self::class, 'mapLatteSourceCode']);
- }
-
$blueScreen->addAction([self::class, 'renderMemberAccessException']);
$blueScreen->addPanel([self::class, 'renderNeonError']);
}
- public static function renderLatteError(?\Throwable $e): ?array
- {
- if ($e instanceof Latte\CompileException && $e->sourceName) {
- return [
- 'tab' => 'Template',
- 'panel' => (preg_match('#\n|\?#', $e->sourceName)
- ? ''
- : ''
- . (@is_file($e->sourceName) // @ - may trigger error
- ? 'File: ' . Helpers::editorLink($e->sourceName, $e->sourceLine)
- : '' . htmlspecialchars($e->sourceName . ($e->sourceLine ? ':' . $e->sourceLine : '')) . '')
- . '
')
- . BlueScreen::highlightFile($e->sourceCode, $e->sourceLine, php: false),
- ];
- }
-
- return null;
- }
-
-
- public static function renderLatteUnknownMacro(?\Throwable $e): ?array
- {
- if (
- $e instanceof Latte\CompileException
- && $e->sourceName
- && @is_file($e->sourceName) // @ - may trigger error
- && (preg_match('#Unknown macro (\{\w+)\}, did you mean (\{\w+)\}\?#A', $e->getMessage(), $m)
- || preg_match('#Unknown attribute (n:\w+), did you mean (n:\w+)\?#A', $e->getMessage(), $m))
- ) {
- return [
- 'link' => Helpers::editorUri($e->sourceName, $e->sourceLine, 'fix', $m[1], $m[2]),
- 'label' => 'fix it',
- ];
- }
-
- return null;
- }
-
-
- /** @return array{file: string, line: int, label: string, active: bool} */
- public static function mapLatteSourceCode(string $file, int $line): ?array
- {
- if (!strpos($file, '.latte--')) {
- return null;
- }
-
- $lines = file($file);
- if (
- !preg_match('#^/(?:\*\*|/) source: (\S+\.latte)#m', implode('', array_slice($lines, 0, 10)), $m)
- || !@is_file($m[1]) // @ - may trigger error
- ) {
- return null;
- }
-
- $file = $m[1];
- $line = $line && preg_match('#/\* line (\d+) \*/#', $lines[$line - 1], $m) ? (int) $m[1] : 0;
- return ['file' => $file, 'line' => $line, 'label' => 'Latte', 'active' => true];
- }
-
-
public static function renderMemberAccessException(?\Throwable $e): ?array
{
if (!$e instanceof Nette\MemberAccessException && !$e instanceof \LogicException) {
diff --git a/src/Bridges/Nette/TracyExtension.php b/src/Bridges/Nette/TracyExtension.php
index 3f0c6ac2b..809ef50e4 100644
--- a/src/Bridges/Nette/TracyExtension.php
+++ b/src/Bridges/Nette/TracyExtension.php
@@ -130,7 +130,7 @@ public function afterCompile(Nette\PhpGenerator\ClassType $class): void
if ($this->debugMode) {
foreach ($this->config->bar as $item) {
if (is_string($item) && substr($item, 0, 1) === '@') {
- $item = new Statement(['@' . $builder::THIS_CONTAINER, 'getService'], [substr($item, 1)]);
+ $item = new Statement(['@' . $builder::ThisContainer, 'getService'], [substr($item, 1)]);
} elseif (is_string($item)) {
$item = new Statement($item);
}
diff --git a/src/Tracy/Bar/Bar.php b/src/Tracy/Bar/Bar.php
index 037bc3cee..5b635e379 100644
--- a/src/Tracy/Bar/Bar.php
+++ b/src/Tracy/Bar/Bar.php
@@ -15,10 +15,13 @@
*/
class Bar
{
- /** @var IBarPanel[] */
+ /** @var array */
private array $panels = [];
private bool $loaderRendered = false;
+ /** @var ?callable(string, string): int */
+ private $panelSorter;
+
/**
* Add custom panel.
@@ -37,6 +40,13 @@ public function addPanel(IBarPanel $panel, ?string $id = null): self
return $this;
}
+ /**
+ * @param callable(string, string): int $sorter
+ */
+ public function replacePanelSorter(callable $sorter): void
+ {
+ $this->panelSorter = $sorter;
+ }
/**
* Returns panel with given id
@@ -136,6 +146,10 @@ private function renderPanels(string $suffix = ''): array
$obLevel = ob_get_level();
$panels = [];
+ if($this->panelSorter !== null) {
+ uksort($this->panels, $this->panelSorter);
+ }
+
foreach ($this->panels as $id => $panel) {
$idHtml = preg_replace('#[^a-z0-9]+#i', '-', $id) . $suffix;
try {
diff --git a/src/Tracy/BlueScreen/BlueScreen.php b/src/Tracy/BlueScreen/BlueScreen.php
index 656058d67..fb220a05b 100644
--- a/src/Tracy/BlueScreen/BlueScreen.php
+++ b/src/Tracy/BlueScreen/BlueScreen.php
@@ -499,4 +499,18 @@ private function findGeneratorsAndFibers(object $object): array
Helpers::traverseValue($object, $add);
return [$generators, $fibers];
}
+
+
+ public function getRealArgsAndVariables(\Throwable $exception): array
+ {
+ $args = $variables = [];
+ if (function_exists('xdebug_get_function_stack') && version_compare(phpversion('xdebug'), '3.3.0', '>=')) {
+ $stack = xdebug_get_function_stack(['from_exception' => $exception]);
+ foreach (array_reverse($stack) as $k => $row) {
+ $args[$k] = $row['params'] ?? [];
+ $variables[$k - 1] = $row['variables'] ?? [];
+ }
+ }
+ return [$args, $variables];
+ }
}
diff --git a/src/Tracy/BlueScreen/assets/bluescreen.css b/src/Tracy/BlueScreen/assets/bluescreen.css
index bbe86c943..f63c29569 100644
--- a/src/Tracy/BlueScreen/assets/bluescreen.css
+++ b/src/Tracy/BlueScreen/assets/bluescreen.css
@@ -350,11 +350,15 @@ html.tracy-bs-visible body {
grid-column-end: 3;
}
-#tracy-bs .tracy-callstack-args tr:first-child > * {
+#tracy-bs .tracy-callstack-args tr > :first-child {
+ width: 10em;
+}
+
+#tracy-bs .tracy-callstack-args-warning tr:first-child > * {
position: relative;
}
-#tracy-bs .tracy-callstack-args tr:first-child td:before {
+#tracy-bs .tracy-callstack-args-warning tr:first-child td:before {
position: absolute;
right: .3em;
content: 'may not be true';
diff --git a/src/Tracy/BlueScreen/assets/section-stack-callStack.phtml b/src/Tracy/BlueScreen/assets/section-stack-callStack.phtml
index 74b3744d8..c38da5a40 100644
--- a/src/Tracy/BlueScreen/assets/section-stack-callStack.phtml
+++ b/src/Tracy/BlueScreen/assets/section-stack-callStack.phtml
@@ -8,6 +8,8 @@ namespace Tracy;
* @var callable $dump
* @var int $expanded
* @var array $stack
+ * @var ?array $realArgs
+ * @var ?array $variables
*/
if (!$stack) {
@@ -66,15 +68,27 @@ if (!$stack) {
-
+
$v) {
+ echo '| ', Helpers::escapeHtml((is_string($argName) ? '$' : '#') . $argName), ' | ';
+ echo $dump($v, $argName);
+ echo " |
\n";
+ }
+?>
+
+
+
+
+getParameters();
- } catch (\Exception) {
+ } else {
$params = [];
}
+
foreach ($row['args'] as $k => $v) {
$argName = isset($params[$k]) && !$params[$k]->isVariadic() ? $params[$k]->name : $k;
echo '| ', Helpers::escapeHtml((is_string($argName) ? '$' : '#') . $argName), ' | ';
@@ -84,6 +98,20 @@ if (!$stack) {
?>
|
|---|
+
+
+ Local Variables
+
+
+ $v) {
+ echo '| $', Helpers::escapeHtml($k), ' | ';
+ echo $dump($v, $k);
+ echo " |
\n";
+ }
+?>
+
+
diff --git a/src/Tracy/BlueScreen/assets/section-stack-exception.phtml b/src/Tracy/BlueScreen/assets/section-stack-exception.phtml
index 9e9bc66de..e3d459832 100644
--- a/src/Tracy/BlueScreen/assets/section-stack-exception.phtml
+++ b/src/Tracy/BlueScreen/assets/section-stack-exception.phtml
@@ -33,6 +33,8 @@ if (($stack[0]['class'] ?? null) === Debugger::class && in_array($stack[0]['func
}
$file = $ex->getFile();
$line = $ex->getLine();
+[$realArgs, $variables] = $this->getRealArgsAndVariables($ex);
require __DIR__ . '/section-stack-sourceFile.phtml';
+require __DIR__ . '/section-stack-variables.phtml';
require __DIR__ . '/section-stack-callStack.phtml';
diff --git a/src/Tracy/BlueScreen/assets/section-stack-variables.phtml b/src/Tracy/BlueScreen/assets/section-stack-variables.phtml
new file mode 100644
index 000000000..6fc766421
--- /dev/null
+++ b/src/Tracy/BlueScreen/assets/section-stack-variables.phtml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ $v) {
+ echo '| $', Helpers::escapeHtml($k), ' | ';
+ echo $dump($v, $k);
+ echo " |
\n";
+ }
+?>
+
+
+
diff --git a/src/Tracy/Debugger/Debugger.php b/src/Tracy/Debugger/Debugger.php
index 7e0fdeef5..407e52433 100644
--- a/src/Tracy/Debugger/Debugger.php
+++ b/src/Tracy/Debugger/Debugger.php
@@ -17,7 +17,7 @@
*/
class Debugger
{
- public const Version = '2.10.5';
+ public const Version = '3.0-dev';
/** server modes for Debugger::enable() */
public const
@@ -289,7 +289,7 @@ public static function shutdownHandler(): void
{
$error = error_get_last();
if (in_array($error['type'] ?? null, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR], true)) {
- self::exceptionHandler(Helpers::fixStack(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'])));
+ self::exceptionHandler(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']));
} elseif (($error['type'] ?? null) === E_COMPILE_WARNING) {
error_clear_last();
self::errorHandler($error['type'], $error['message'], $error['file'], $error['line']);
diff --git a/src/Tracy/Dumper/Describer.php b/src/Tracy/Dumper/Describer.php
index e841a3fa4..a0d293c68 100644
--- a/src/Tracy/Dumper/Describer.php
+++ b/src/Tracy/Dumper/Describer.php
@@ -326,19 +326,19 @@ private static function findLocation(): ?array
if (isset($item['class']) && ($item['class'] === self::class || $item['class'] === Tracy\Dumper::class)) {
$location = $item;
continue;
- } elseif (isset($item['function'])) {
- try {
- $reflection = isset($item['class'])
- ? new \ReflectionMethod($item['class'], $item['function'])
- : new \ReflectionFunction($item['function']);
- if (
- $reflection->isInternal()
- || preg_match('#\s@tracySkipLocation\s#', (string) $reflection->getDocComment())
- ) {
- $location = $item;
- continue;
- }
- } catch (\ReflectionException) {
+ } elseif (
+ isset($item['function'])
+ && (isset($item['class']) ? method_exists($item['class'], $item['function']) : function_exists($item['function']))
+ ) {
+ $reflection = isset($item['class'])
+ ? new \ReflectionMethod($item['class'], $item['function'])
+ : new \ReflectionFunction($item['function']);
+ if (
+ $reflection->isInternal()
+ || preg_match('#\s@tracySkipLocation\s#', (string) $reflection->getDocComment())
+ ) {
+ $location = $item;
+ continue;
}
}
diff --git a/src/Tracy/Helpers.php b/src/Tracy/Helpers.php
index 7bcdcbc7f..4bbd6cf7e 100644
--- a/src/Tracy/Helpers.php
+++ b/src/Tracy/Helpers.php
@@ -116,37 +116,6 @@ public static function findTrace(array $trace, array|string $method, ?int &$inde
}
- /** @internal */
- public static function fixStack(\Throwable $exception): \Throwable
- {
- if (function_exists('xdebug_get_function_stack')) {
- $stack = [];
- $trace = @xdebug_get_function_stack(); // @ xdebug compatibility warning
- $trace = array_slice(array_reverse($trace), 2, -1);
- foreach ($trace as $row) {
- $frame = [
- 'file' => $row['file'],
- 'line' => $row['line'],
- 'function' => $row['function'] ?? '*unknown*',
- 'args' => [],
- ];
- if (!empty($row['class'])) {
- $frame['type'] = isset($row['type']) && $row['type'] === 'dynamic' ? '->' : '::';
- $frame['class'] = $row['class'];
- }
-
- $stack[] = $frame;
- }
-
- $ref = new \ReflectionProperty('Exception', 'trace');
- $ref->setAccessible(true);
- $ref->setValue($exception, $stack);
- }
-
- return $exception;
- }
-
-
/** @internal */
public static function errorTypeToString(int $type): string
{
@@ -194,42 +163,67 @@ public static function improveException(\Throwable $e): void
{
$message = $e->getMessage();
if (
- (!$e instanceof \Error && !$e instanceof \ErrorException)
- || $e instanceof Nette\MemberAccessException
- || strpos($e->getMessage(), 'did you mean')
+ !($e instanceof \Error || $e instanceof \ErrorException)
+ || str_contains($e->getMessage(), 'did you mean')
) {
// do nothing
+ } elseif (preg_match('~Argument #(\d+)(?: \(\$\w+\))? must be of type callable, (.+ given)~', $message, $m)) {
+ $arg = $e->getTrace()[0]['args'][$m[1] - 1] ?? null;
+ if (is_string($arg) && str_contains($arg, '::')) {
+ $arg = explode('::', $arg, 2);
+ }
+ if (!is_callable($arg, syntax_only: true)) {
+ // do nothing
+ } elseif (is_array($arg) && is_string($arg[0]) && !class_exists($arg[0]) && !trait_exists($arg[0])) {
+ $message = str_replace($m[2], "but class '$arg[0]' does not exist", $message);
+ } elseif (is_array($arg) && !method_exists($arg[0], $arg[1])) {
+ $hint = self::getSuggestion(get_class_methods($arg[0]) ?: [], $arg[1]);
+ $class = is_object($arg[0]) ? get_class($arg[0]) : $arg[0];
+ $message = str_replace($m[2], "but method $class::$arg[1]() does not exist" . ($hint ? " (did you mean $hint?)" : ''), $message);
+ } elseif (is_string($arg) && !function_exists($arg)) {
+ $funcs = array_merge(get_defined_functions()['internal'], get_defined_functions()['user']);
+ $hint = self::getSuggestion($funcs, $arg);
+ $message = str_replace($m[2], "but function '$arg' does not exist" . ($hint ? " (did you mean $hint?)" : ''), $message);
+ }
+
} elseif (preg_match('#^Call to undefined function (\S+\\\\)?(\w+)\(#', $message, $m)) {
$funcs = array_merge(get_defined_functions()['internal'], get_defined_functions()['user']);
- $hint = self::getSuggestion($funcs, $m[1] . $m[2]) ?: self::getSuggestion($funcs, $m[2]);
- $message = "Call to undefined function $m[2](), did you mean $hint()?";
- $replace = ["$m[2](", "$hint("];
+ if ($hint = self::getSuggestion($funcs, $m[1] . $m[2]) ?: self::getSuggestion($funcs, $m[2])) {
+ $message = "Call to undefined function $m[2](), did you mean $hint()?";
+ $replace = ["$m[2](", "$hint("];
+ }
} elseif (preg_match('#^Call to undefined method ([\w\\\\]+)::(\w+)#', $message, $m)) {
- $hint = self::getSuggestion(get_class_methods($m[1]) ?: [], $m[2]);
- $message .= ", did you mean $hint()?";
- $replace = ["$m[2](", "$hint("];
+ if ($hint = self::getSuggestion(get_class_methods($m[1]) ?: [], $m[2])) {
+ $message .= ", did you mean $hint()?";
+ $replace = ["$m[2](", "$hint("];
+ }
} elseif (preg_match('#^Undefined property: ([\w\\\\]+)::\$(\w+)#', $message, $m)) {
$rc = new \ReflectionClass($m[1]);
$items = array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), fn($prop) => !$prop->isStatic());
- $hint = self::getSuggestion($items, $m[2]);
- $message .= ", did you mean $$hint?";
- $replace = ["->$m[2]", "->$hint"];
+ if ($hint = self::getSuggestion($items, $m[2])) {
+ $message .= ", did you mean $$hint?";
+ $replace = ["->$m[2]", "->$hint"];
+ }
} elseif (preg_match('#^Access to undeclared static property:? ([\w\\\\]+)::\$(\w+)#', $message, $m)) {
$rc = new \ReflectionClass($m[1]);
$items = array_filter($rc->getProperties(\ReflectionProperty::IS_STATIC), fn($prop) => $prop->isPublic());
- $hint = self::getSuggestion($items, $m[2]);
- $message .= ", did you mean $$hint?";
- $replace = ["::$$m[2]", "::$$hint"];
+ if ($hint = self::getSuggestion($items, $m[2])) {
+ $message .= ", did you mean $$hint?";
+ $replace = ["::$$m[2]", "::$$hint"];
+ }
}
- if (isset($hint)) {
- $loc = Debugger::mapSource($e->getFile(), $e->getLine()) ?? ['file' => $e->getFile(), 'line' => $e->getLine()];
+ if ($message !== $e->getMessage()) {
$ref = new \ReflectionProperty($e, 'message');
$ref->setAccessible(true);
$ref->setValue($e, $message);
+ }
+
+ if (isset($replace)) {
+ $loc = Debugger::mapSource($e->getFile(), $e->getLine()) ?? ['file' => $e->getFile(), 'line' => $e->getLine()];
@$e->tracyAction = [ // dynamic properties are deprecated since PHP 8.2
'link' => self::editorUri($loc['file'], $loc['line'], 'fix', $replace[0], $replace[1]),
'label' => 'fix it',
@@ -544,6 +538,7 @@ function ($match) use (&$last) {
/** @internal */
public static function minifyCss(string $s): string
{
+ return $s;
$last = '';
return preg_replace_callback(
<<<'XX'
diff --git a/src/Tracy/Logger/Logger.php b/src/Tracy/Logger/Logger.php
index 4ef677798..7c9e65bc5 100644
--- a/src/Tracy/Logger/Logger.php
+++ b/src/Tracy/Logger/Logger.php
@@ -15,23 +15,22 @@
*/
class Logger implements ILogger
{
- /** @var string|null name of the directory where errors should be logged */
- public $directory;
+ /** name of the directory where errors should be logged */
+ public ?string $directory = null;
- /** @var string|array|null email or emails to which send error notifications */
- public $email;
+ /** email or emails to which send error notifications */
+ public string|array|null $email = null;
- /** @var string|null sender of email notifications */
- public $fromEmail;
+ /** sender of email notifications */
+ public ?string $fromEmail = null;
- /** @var mixed interval for sending email is 2 days */
- public $emailSnooze = '2 days';
+ /** interval for sending email is 2 days */
+ public mixed $emailSnooze = '2 days';
/** @var callable handler for sending emails */
public $mailer;
- /** @var BlueScreen|null */
- private $blueScreen;
+ private ?BlueScreen $blueScreen = null;
public function __construct(?string $directory, string|array|null $email = null, ?BlueScreen $blueScreen = null)
@@ -78,10 +77,7 @@ public function log(mixed $message, string $level = self::INFO)
}
- /**
- * @param mixed $message
- */
- public static function formatMessage($message): string
+ public static function formatMessage(mixed $message): string
{
if ($message instanceof \Throwable) {
foreach (Helpers::getExceptionChain($message) as $exception) {
@@ -101,10 +97,7 @@ public static function formatMessage($message): string
}
- /**
- * @param mixed $message
- */
- public static function formatLogLine($message, ?string $exceptionFile = null): string
+ public static function formatLogLine(mixed $message, ?string $exceptionFile = null): string
{
return implode(' ', [
date('[Y-m-d H-i-s]'),
@@ -152,10 +145,7 @@ protected function logException(\Throwable $exception, ?string $file = null): st
}
- /**
- * @param mixed $message
- */
- protected function sendEmail($message): void
+ protected function sendEmail(mixed $message): void
{
$snooze = is_numeric($this->emailSnooze)
? $this->emailSnooze
@@ -174,10 +164,9 @@ protected function sendEmail($message): void
/**
* Default mailer.
- * @param mixed $message
* @internal
*/
- public function defaultMailer($message, string $email): void
+ public function defaultMailer(mixed $message, string $email): void
{
$host = preg_replace('#[^\w.-]+#', '', $_SERVER['SERVER_NAME'] ?? php_uname('n'));
$parts = str_replace(
diff --git a/tests/Tracy/Debugger.E_COMPILE_ERROR.console.phpt b/tests/Tracy/Debugger.E_COMPILE_ERROR.console.phpt
index ab566eacf..bcf29eb2b 100644
--- a/tests/Tracy/Debugger.E_COMPILE_ERROR.console.phpt
+++ b/tests/Tracy/Debugger.E_COMPILE_ERROR.console.phpt
@@ -24,16 +24,7 @@ $onFatalErrorCalled = false;
register_shutdown_function(function () use (&$onFatalErrorCalled) {
Assert::true($onFatalErrorCalled);
- Assert::match(extension_loaded('xdebug') ?
-'ErrorException: Cannot re-assign $this in %a%
-Stack trace:
-#0 %a%: third()
-#1 %a%: second()
-#2 %a%: first()
-#3 {main}
-Tracy is unable to log error: Logging directory is not specified.
-' :
-'ErrorException: Cannot re-assign $this in %a%
+ Assert::match('ErrorException: Cannot re-assign $this in %a%
Stack trace:
#0 [internal function]: Tracy\\Debugger::shutdownHandler()
#1 {main}
diff --git a/tests/Tracy/Helpers.improveException.phpt b/tests/Tracy/Helpers.improveException.phpt
index 8ee2ba7e8..32e78325a 100644
--- a/tests/Tracy/Helpers.improveException.phpt
+++ b/tests/Tracy/Helpers.improveException.phpt
@@ -218,3 +218,64 @@ test('do not suggest anything when accessing anonymous class', function () {
Assert::same('Undefined property: class@anonymous::$property', $e->getMessage());
Assert::false(isset($e->tracyAction));
});
+
+
+test('callable error: ignore syntax mismatch', function () {
+ try {
+ (fn(callable $a) => null)(null);
+ } catch (Error $e) {
+ }
+
+ Helpers::improveException($e);
+ Assert::match('{closure}(): Argument #1 ($a) must be of type callable, null given, called in %a%', $e->getMessage());
+});
+
+test('callable error: typo in class name', function () {
+ try {
+ (fn(callable $a) => null)([PhpTokn::class, 'tokenize']);
+ } catch (Error $e) {
+ }
+
+ Helpers::improveException($e);
+ Assert::match("{closure}(): Argument #1 (\$a) must be of type callable, but class 'PhpTokn' does not exist, called in %a%", $e->getMessage());
+});
+
+test('callable error: typo in class name', function () {
+ try {
+ (fn(callable $a) => null)('PhpTokn::tokenize');
+ } catch (Error $e) {
+ }
+
+ Helpers::improveException($e);
+ Assert::match("{closure}(): Argument #1 (\$a) must be of type callable, but class 'PhpTokn' does not exist, called in %a%", $e->getMessage());
+});
+
+test('callable error: typo in method name', function () {
+ try {
+ (fn(callable $a) => null)([PhpToken::class, 'tokenze']);
+ } catch (Error $e) {
+ }
+
+ Helpers::improveException($e);
+ Assert::match('{closure}(): Argument #1 ($a) must be of type callable, but method PhpToken::tokenze() does not exist (did you mean tokenize?), called in %a%', $e->getMessage());
+});
+
+test('callable error: typo in method name', function () {
+ try {
+ (fn(callable $a) => null)('PhpToken::tokenze');
+ } catch (Error $e) {
+ }
+
+ Helpers::improveException($e);
+ Assert::match('{closure}(): Argument #1 ($a) must be of type callable, but method PhpToken::tokenze() does not exist (did you mean tokenize?), called in %a%', $e->getMessage());
+});
+
+test('callable error: typo in function name', function () {
+ try {
+ (fn(callable $a) => null)('trm');
+ } catch (Error $e) {
+ }
+
+ Helpers::improveException($e);
+ Assert::match("{closure}(): Argument #1 (\$a) must be of type callable, but function 'trm' does not exist (did you mean trim?), called in %a%", $e->getMessage());
+});