diff --git a/src/Tracy/Helpers.php b/src/Tracy/Helpers.php index 00aa36ac7..684d3a8fb 100644 --- a/src/Tracy/Helpers.php +++ b/src/Tracy/Helpers.php @@ -168,6 +168,25 @@ public static function improveException(\Throwable $e): void || strpos($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']); if ($hint = self::getSuggestion($funcs, $m[1] . $m[2]) ?: self::getSuggestion($funcs, $m[2])) { diff --git a/tests/Tracy/Helpers.improveException.phpt b/tests/Tracy/Helpers.improveException.phpt index 8ee2ba7e8..208e00276 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)(false); + } catch (Error $e) { + } + + Helpers::improveException($e); + Assert::match('{closure}(): Argument #1 ($a) must be of type callable, bool 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()); +});