Skip to content

Commit c8fc8c4

Browse files
committed
Fix Error when the trace has Twig file/line information instead of the original PHP info
1 parent 02118c7 commit c8fc8c4

5 files changed

+131
-2
lines changed

src/Error/Error.php

+10-2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class Error extends \Exception
4141
private $lineno;
4242
private $rawMessage;
4343
private ?Source $source;
44+
private string $phpFile;
45+
private int $phpLine;
4446

4547
/**
4648
* Constructor.
@@ -55,6 +57,8 @@ public function __construct(string $message, int $lineno = -1, ?Source $source =
5557
{
5658
parent::__construct('', 0, $previous);
5759

60+
$this->phpFile = $this->getFile();
61+
$this->phpLine = $this->getLine();
5862
$this->lineno = $lineno;
5963
$this->source = $source;
6064
$this->rawMessage = $message;
@@ -111,6 +115,8 @@ private function updateRepr(): void
111115
$this->file = $this->source->getPath();
112116
if ($this->lineno > 0) {
113117
$this->line = $this->lineno;
118+
} else {
119+
$this->line = -1;
114120
}
115121
}
116122

@@ -134,6 +140,7 @@ private function guessTemplateInfo(): void
134140
{
135141
// $this->source is never null here (see guess() usage in Template)
136142

143+
$this->lineno = 0;
137144
$template = null;
138145
$templateClass = null;
139146
$backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT);
@@ -144,6 +151,8 @@ private function guessTemplateInfo(): void
144151
if ($this->source->getName() === $trace['object']->getTemplateName() && !$isEmbedContainer) {
145152
$template = $trace['object'];
146153
$templateClass = \get_class($trace['object']);
154+
155+
break;
147156
}
148157
}
149158
}
@@ -158,8 +167,7 @@ private function guessTemplateInfo(): void
158167

159168
while ($e = array_pop($exceptions)) {
160169
$traces = $e->getTrace();
161-
array_unshift($traces, ['file' => $e->getFile(), 'line' => $e->getLine()]);
162-
170+
array_unshift($traces, ['file' => $e instanceof Error ? $e->phpFile : $e->getFile(), 'line' => $e instanceof Error ? $e->phpLine : $e->getLine()]);
163171
while ($trace = array_shift($traces)) {
164172
if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) {
165173
continue;

tests/ErrorTest.php

+112
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@
1212
*/
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Twig\Attribute\YieldReady;
16+
use Twig\Compiler;
1517
use Twig\Environment;
1618
use Twig\Error\Error;
1719
use Twig\Error\RuntimeError;
1820
use Twig\Error\SyntaxError;
1921
use Twig\Loader\ArrayLoader;
2022
use Twig\Loader\FilesystemLoader;
23+
use Twig\Loader\LoaderInterface;
24+
use Twig\Node\Node;
2125
use Twig\Source;
26+
use Twig\Token;
27+
use Twig\TokenParser\AbstractTokenParser;
2228

2329
class ErrorTest extends TestCase
2430
{
@@ -253,6 +259,112 @@ public function testTwigExceptionUpdateFileAndLineTogether()
253259
}
254260
}
255261

262+
/**
263+
* @dataProvider getErrorWithoutLineAndContextData
264+
*/
265+
public function testErrorWithoutLineAndContext(LoaderInterface $loader, bool $debug, bool $addDebugInfo, bool $exceptionWithLineAndContext, int $errorLine)
266+
{
267+
$twig = new Environment($loader, ['debug' => $debug, 'cache' => false]);
268+
$twig->removeCache('no_line_and_context_exception.twig');
269+
$twig->removeCache('no_line_and_context_exception_include_line_5.twig');
270+
$twig->removeCache('no_line_and_context_exception_include_line_1.twig');
271+
$twig->addTokenParser(new class($addDebugInfo, $exceptionWithLineAndContext) extends AbstractTokenParser {
272+
public function __construct(private bool $addDebugInfo, private bool $exceptionWithLineAndContext)
273+
{
274+
}
275+
276+
public function parse(Token $token)
277+
{
278+
$stream = $this->parser->getStream();
279+
$lineno = $stream->getCurrent()->getLine();
280+
$stream->expect(Token::BLOCK_END_TYPE);
281+
282+
return new #[YieldReady]class($lineno, $this->addDebugInfo, $this->exceptionWithLineAndContext) extends Node
283+
{
284+
public function __construct(int $lineno, private bool $addDebugInfo, private bool $exceptionWithLineAndContext)
285+
{
286+
parent::__construct([], [], $lineno);
287+
}
288+
289+
public function compile(Compiler $compiler): void
290+
{
291+
if ($this->addDebugInfo) {
292+
$compiler->addDebugInfo($this);
293+
}
294+
if ($this->exceptionWithLineAndContext) {
295+
$compiler
296+
->write('throw new \Twig\Error\RuntimeError("Runtime error.", ')
297+
->repr($this->lineno)->raw(", \$this->getSourceContext()")
298+
->raw(");\n")
299+
;
300+
} else {
301+
$compiler->write('throw new \Twig\Error\RuntimeError("Runtime error.");');
302+
}
303+
}
304+
};
305+
}
306+
307+
public function getTag()
308+
{
309+
return 'foo';
310+
}
311+
});
312+
313+
try {
314+
//echo $twig->compile($twig->parse($twig->tokenize(new \Twig\Source($twig->getLoader()->getSourceContext('no_line_and_context_exception_include_line_'.$errorLine.'.twig')->getCode(), 'no_line_and_context_exception.twig'))))."\n";
315+
$twig->render('no_line_and_context_exception.twig', ['line' => $errorLine]);
316+
$this->fail();
317+
} catch (RuntimeError $e) {
318+
$line = $addDebugInfo || $exceptionWithLineAndContext ? $errorLine : 1;
319+
320+
if (1 === $errorLine && !$addDebugInfo && !$exceptionWithLineAndContext) {
321+
$this->assertSame(\sprintf('Runtime error in "no_line_and_context_exception_include_line_%d.twig".', $errorLine), $e->getMessage());
322+
$this->assertSame(0, $e->getTemplateLine());
323+
} else {
324+
$this->assertSame(\sprintf('Runtime error in "no_line_and_context_exception_include_line_%d.twig" at line %d.', $errorLine, $line), $e->getMessage());
325+
$this->assertSame($line, $e->getTemplateLine());
326+
}
327+
328+
if ($loader instanceof FilesystemLoader) {
329+
$this->assertStringContainsString(\sprintf('errors/no_line_and_context_exception_include_line_%d.twig', $errorLine), $e->getFile());
330+
$this->assertSame($addDebugInfo || $exceptionWithLineAndContext ? $errorLine : (1 === $errorLine ? -1 : 1), $e->getLine());
331+
} else {
332+
$this->assertStringContainsString('Environment.php', $e->getFile());
333+
$this->assertNotSame($line, $e->getLine());
334+
}
335+
}
336+
}
337+
338+
public static function getErrorWithoutLineAndContextData(): iterable
339+
{
340+
$fileLoaders = [
341+
new ArrayLoader([
342+
'no_line_and_context_exception.twig' => "\n\n{{ include('no_line_and_context_exception_include_line_' ~ line ~ '.twig') }}",
343+
'no_line_and_context_exception_include_line_5.twig' => "\n\n\n\n{% foo %}",
344+
'no_line_and_context_exception_include_line_1.twig' => '{% foo %}',
345+
]),
346+
new FilesystemLoader(__DIR__.'/Fixtures/errors'),
347+
];
348+
349+
foreach ($fileLoaders as $loader) {
350+
foreach ([false, true] as $exceptionWithLineAndContext) {
351+
foreach ([false, true] as $addDebugInfo) {
352+
foreach ([false, true] as $debug) {
353+
foreach ([5, 1] as $line) {
354+
$name = ($loader instanceof FilesystemLoader ? 'filesystem' : 'array')
355+
.($debug ? '_with_debug' : '_without_debug')
356+
.($addDebugInfo ? '_with_debug_info' : '_without_debug_info')
357+
.($exceptionWithLineAndContext ? '_with_context' : '_without_context')
358+
.('_line_'.$line)
359+
;
360+
yield $name => [$loader, $debug, $addDebugInfo, $exceptionWithLineAndContext, $line];
361+
}
362+
}
363+
}
364+
}
365+
}
366+
}
367+
256368
public static function getErroredTemplates()
257369
{
258370
return [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
3+
{{ include('no_line_and_context_exception_include_line_' ~ line ~ '.twig') }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{% foo %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
3+
4+
5+
{% foo %}

0 commit comments

Comments
 (0)