Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Fix #172: Address review comments
  • Loading branch information
WarLikeLaux committed Mar 29, 2026
commit cbb1aa4790b3c0a971f92afdf5c74907d38f8b07
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
- Enh #163: Explicitly import classes, functions, and constants in "use" section (@mspirkov)
- Bug #164: Fix missing items in stack trace HTML output when handling a PHP error (@vjik)
- Bug #166: Fix broken link to error handling guide (@vjik)
- Bug #172: Fix closure rendering in stack traces and keep items when source is unavailable (@WarLikeLaux)
- Enh #172: Improve closure rendering in stack traces (@WarLikeLaux)
- Bug #172: Keep items in stack traces when source is unavailable (@WarLikeLaux)

## 4.3.2 January 09, 2026

Expand Down
15 changes: 10 additions & 5 deletions src/Renderer/HtmlRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -768,12 +768,17 @@ private function renderCallStackItem(
if ($file !== null && $line !== null) {
$line--; // adjust line number from one-based to zero-based
$lines = @file($file);
if ($line >= 0 && $lines !== false && ($lineCount = count($lines)) > $line) {
$half = (int) (($index === 1 ? $this->maxSourceLines : $this->maxTraceLines) / 2);
$begin = $line - $half > 0 ? $line - $half : 0;
$end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
} else {
if ($line < 0 || $lines === false) {
$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) {

$half = (int) (($index === 1 ? $this->maxSourceLines : $this->maxTraceLines) / 2);
$begin = $line - $half > 0 ? $line - $half : 0;
$end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
} else {
$lines = [];
}
}
}

Expand Down
41 changes: 23 additions & 18 deletions tests/Renderer/HtmlRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,25 @@ public function testRenderCallStackItemDoesNotRenderSourceCodeWhenLineIsOutsideF
$this->assertStringNotContainsString('element-code-wrap', $result);
}

public function testRenderCallStackItemRendersSourceCodeForLastLineInFile(): void
{
$line = count(file(__FILE__));
$result = $this->invokeMethod(new HtmlRenderer(), 'renderCallStackItem', [
'file' => __FILE__,
'line' => $line,
'class' => null,
'function' => null,
'args' => [],
'index' => 1,
'isVendorFile' => false,
'reflectionParameters' => [],
]);

$this->assertStringContainsString(__FILE__, $result);
$this->assertStringContainsString('at line ' . $line, $result);
$this->assertStringContainsString('element-code-wrap', $result);
}

public function testRenderRequest(): void
{
$renderer = new HtmlRenderer();
Expand Down Expand Up @@ -669,7 +688,7 @@ public static function dataFormatTraceFunctionName(): iterable
public function testFormatTraceFunctionName(?string $class, string $function, string $expected): void
{
$renderer = new HtmlRenderer();
$this->assertSame($expected, $renderer->formatTraceFunctionName($class, $function));
$this->assertSame($expected, $this->invokeMethod($renderer, 'formatTraceFunctionName', [$class, $function]));
}

public function testRenderCallStackWithMethodClosure(): void
Expand All @@ -685,9 +704,6 @@ public function testRenderCallStackWithMethodClosure(): void

$result = $renderer->renderCallStack($exception, $exception->getTrace());

$this->assertStringContainsString('{closure}', $result);
$this->assertStringNotContainsString(self::class . '::' . $traceItem['function'], $result);

if (PHP_VERSION_ID >= 80400) {
$this->assertMatchesRegularExpression(
'/\{closure\}\s+' . preg_quote(self::class, '/') . '::createMethodClosureException\(\):\d+/',
Expand Down Expand Up @@ -720,10 +736,6 @@ public function testRenderCallStackWithBoundClosure(): void
[],
]);

$this->assertStringContainsString('{closure}', $result);
$this->assertStringContainsString('{closure}', $itemResult);
$this->assertStringNotContainsString('Closure::{closure}', $itemResult);

if (PHP_VERSION_ID >= 80400) {
$this->assertMatchesRegularExpression('/\{closure\}\s+.+::createBoundClosureException\(\):\d+/', $result);
return;
Expand Down Expand Up @@ -755,9 +767,6 @@ public function testRenderCallStackWithInternalFunctionClosure(): void
[],
]);

$this->assertStringContainsString('{closure}', $result);
$this->assertStringNotContainsString(self::class . '::' . $traceItem['function'], $result);
$this->assertStringContainsString('{closure}', $itemResult);
$this->assertStringNotContainsString('element-code-wrap', $itemResult);

if (PHP_VERSION_ID >= 80400) {
Expand Down Expand Up @@ -804,7 +813,6 @@ public function testRenderCallStackWithFileLevelClosureOnPhp84Plus(): void
$traceItem = $exception->getTrace()[0];

$this->assertArrayNotHasKey('class', $traceItem);
$this->assertStringContainsString('{closure:', $traceItem['function']);

$result = $renderer->renderCallStack($exception, $exception->getTrace());

Expand Down Expand Up @@ -866,8 +874,7 @@ private function createMethodClosureException(): RuntimeException
return $e;
}

$this->fail('Expected exception from method closure.');
throw new RuntimeException('Unreachable.');
$this->fail('Method closure did not throw RuntimeException.');
}

private function createInternalFunctionClosureException(): RuntimeException
Expand All @@ -882,8 +889,7 @@ private function createInternalFunctionClosureException(): RuntimeException
return $e;
}

$this->fail('Expected exception from closure called via internal function.');
throw new RuntimeException('Unreachable.');
$this->fail('Closure called via internal function did not throw RuntimeException.');
}

private function createBoundClosureException(): RuntimeException
Expand All @@ -901,8 +907,7 @@ private function createBoundClosureException(): RuntimeException
return $e;
}

$this->fail('Expected exception from Closure scope closure.');
throw new RuntimeException('Unreachable.');
$this->fail('Bound closure did not throw RuntimeException.');
}

private function createTestTemplate(string $path, string $templateContents): void
Expand Down
2 changes: 1 addition & 1 deletion tests/Support/NamespacedClosureTraceFixture.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ public static function createException(): RuntimeException
return $e;
}

throw new RuntimeException('Unreachable.');
throw new RuntimeException('Namespaced closure did not throw RuntimeException.');
}
}
2 changes: 1 addition & 1 deletion tests/Support/file_level_closure_exception.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
return $e;
}

throw new RuntimeException('Unreachable.');
throw new RuntimeException('File-level closure did not throw RuntimeException.');
Loading