Skip to content

Improve closure rendering in stack traces (#172)#172

Open
WarLikeLaux wants to merge 5 commits intoyiisoft:masterfrom
WarLikeLaux:render-closure-trace
Open

Improve closure rendering in stack traces (#172)#172
WarLikeLaux wants to merge 5 commits intoyiisoft:masterfrom
WarLikeLaux:render-closure-trace

Conversation

@WarLikeLaux
Copy link
Copy Markdown
Contributor

Q A
Is bugfix? ✔️
New feature?
Docs added?
Tests added? ✔️
Breaks BC?
Fixed issues #106

What does this PR do?

This fixes how closures are shown in the HTML stack trace and stops some trace items from disappearing.

  1. Keep trace items when the source snippet is unavailable. renderCallStackItem() used to return an empty string when the source file could not be read. That removed the whole item from the error page. Now the item still renders with the available file and function data. The source viewer is skipped.

  2. Remove duplicated namespace parts in closure names. On PHP < 8.4, a namespaced closure like Yiisoft\Yii\Gii\{closure} was combined with the class name and ended up as Yiisoft\Yii\Gii\Gii::Yiisoft\Yii\Gii\{closure}. It now renders as Yiisoft\Yii\Gii\Gii::{closure}. Bound closures (class=Closure) also stop showing the extra Closure:: prefix.

  3. Format PHP 8.4+ closure names. PHP 8.4+ uses closure names like {closure:Foo::bar():4} that include the definition location. The HTML template now formats that value as {closure} Foo::bar():4. renderCallStack() also treats both {closure} and {closure:...} as closure frames.

Before / After

Case Before After
Namespaced (PHP < 8.4) Ns\Gii::Ns\{closure} Ns\Gii::{closure}
Bound closure Closure::{closure} {closure}
PHP 8.4+ in method Foo::{closure:Foo::bar():4} {closure} Foo::bar():4
PHP 8.4+ file-level {closure:/app/index.php:12} {closure} /app/index.php:12
Simple (PHP < 8.4) Foo::{closure} Foo::{closure} (unchanged)

Note on missing file/line

Some closures called through internal PHP functions such as array_map do not have file or line in the trace data. In that case the page shows the function name and arguments only, without a source viewer for that frame. This matches Symfony's error handler, which also renders source excerpts only for frames that have a file path. On PHP 8.4+, the formatted closure name still shows where the closure was defined.

BC

No BC break: formatTraceFunctionName() is a new public helper used by the HTML template.

P.S. Existing tests that use custom templates now create temporary template files per test. This avoids random-order interference between tests and does not change runtime behavior.

@WarLikeLaux WarLikeLaux changed the title Improve closure rendering in stack traces Improve closure rendering in stack traces (#172) Mar 27, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.16%. Comparing base (7378764) to head (cbb1aa4).

Additional details and impacted files
@@             Coverage Diff              @@
##             master     #172      +/-   ##
============================================
+ Coverage     86.92%   87.16%   +0.23%     
- Complexity      216      223       +7     
============================================
  Files            19       19              
  Lines           696      709      +13     
============================================
+ Hits            605      618      +13     
  Misses           91       91              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes HTML stack trace rendering for closure frames (including PHP 8.4+ {closure:...} format) and ensures stack frames are not dropped when source snippets can’t be read, addressing #106.

Changes:

  • Keep rendering call stack items even when the source file can’t be read or the requested line is out of range (skip only the source excerpt).
  • Introduce HtmlRenderer::formatTraceFunctionName() and use it in the call-stack item template to normalize closure names (PHP < 8.4 and PHP 8.4+).
  • Add/adjust test fixtures and PHPUnit coverage for closure rendering scenarios (method closures, bound closures, internal-function closures, and file-level closures).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/Renderer/HtmlRenderer.php Adds trace function name formatter, recognizes {closure:...} frames, and keeps frames when source can’t be loaded.
templates/_call-stack-item.php Uses the new formatter to display cleaner function/closure names.
tests/Renderer/HtmlRendererTest.php Expands test coverage for closure formatting and for rendering frames without source excerpts.
tests/Support/NamespacedClosureTraceFixture.php Fixture for PHP < 8.4 namespaced closure trace behavior.
tests/Support/FileLevelClosureLoader.php Helper to load a file-level closure fixture for PHP 8.4+ behavior.
tests/Support/file_level_closure_exception.php File-level closure exception generator used by tests.
CHANGELOG.md Records the bugfix in the unreleased section.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@vjik vjik requested a review from a team March 28, 2026 17:00
@vjik vjik added the status:code review The pull request needs review. label Mar 28, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

$this->assertSame('', $result);
$this->assertStringContainsString('not-exist', $result);
$this->assertStringContainsString('call-stack-item', $result);
$this->assertSame('file(not-exist): Failed to open stream: No such file or directory', $errorMessage);
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test asserts the full warning text from file('not-exist') exactly. That message can vary across OS/PHP builds/locales (e.g., Windows vs Linux), making the test brittle. Prefer asserting key substrings (or simply that an error was captured) rather than an exact full message.

Suggested change
$this->assertSame('file(not-exist): Failed to open stream: No such file or directory', $errorMessage);
$this->assertNotNull($errorMessage);

Copilot uses AI. Check for mistakes.
<?php

declare(strict_types=1);

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

file_level_closure_exception.php uses RuntimeException multiple times but doesn't import it, while other test support fixtures consistently use RuntimeException;. For consistency (and to match the project's explicit-import convention), add use RuntimeException; after the declare(strict_types=1); line.

Suggested change
use RuntimeException;

Copilot uses AI. Check for mistakes.
Comment on lines 34 to +46
use function dirname;
use function file_exists;
use function file_put_contents;
use function fopen;
use function Yiisoft\ErrorHandler\Tests\Support\loadFileLevelClosureException;
use function unlink;
use function sprintf;
use function count;

use const DIRECTORY_SEPARATOR;
use const PHP_VERSION_ID;

require_once dirname(__DIR__) . '/Support/FileLevelClosureLoader.php';
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file follows an explicit use function ... import style, but the newly added code uses additional global functions (file, tempnam, sys_get_temp_dir, preg_quote, array_map, etc.) without importing them. Please add the missing use function imports (or otherwise make the usage consistent) to avoid mixing styles within the same file.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

@vjik vjik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The edits are similar to LLM usage without review. Am I right?

$lines = [];
} else {
$lineCount = count($lines);
if ($line < $lineCount) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ($line < $lineCount) {
if ($line <= $lineCount) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status:code review The pull request needs review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants