Error Reporting
This package can be installed separately with
composer require phplrt/exception
An error that says "syntax error at offset 137" is technically correct and practically useless. Phplrt errors point at the source:
1error[UnexpectedTokenException]: Syntax error, unexpected "3" (T_NUMBER), T_PLUS expected 2 --> expr.txt:2:1 3 | 41 | 1 + 2 52 | 3 * (4 + ) 6 | ^ 73 |
This works out of the box: install phplrt/exception and any parser or lexer
exception renders like this when converted to a string.
Catching Syntax Errors
1use Phplrt\Parser\Exception\UnexpectedTokenException; 2use Phplrt\Source\VirtualFile; 3 4try { 5 $parser->parse(new VirtualFile('expr.txt', $input)); 6} catch (UnexpectedTokenException $e) { 7 echo $e->getMessage(); // Syntax error, unexpected "3" (T_NUMBER), T_PLUS expected 8 echo $e; // ...plus the snippet above 9}
The exception carries everything needed to build your own message:
1$e->token; // the token it choked on 2$e->token->name; // T_NUMBER 3$e->token->offset; // 6 4$e->source; // the source it was reading
Give Your Sources A Name
This is the one thing you have to do yourself. A bare Source has no name,
so an error can only show the snippet:
$parser->parse(new Source($input));
1error[UnexpectedTokenException]: Syntax error, unexpected "3" (T_NUMBER), T_PLUS expected 2 | 31 | 3 4 | ^
Wrap the input in a File or a VirtualFile and the error can say where:
$parser->parse(new VirtualFile('user-input.txt', $input));
--> user-input.txt:2:1
VirtualFile costs nothing - it is a string with a name attached - so use it
even when the input never touched the disk.
Catching Everything
The contracts give you one interface per stage, which is usually the right granularity:
1use Phplrt\Contracts\Lexer\Exception\LexerExceptionInterface; 2use Phplrt\Contracts\Parser\Exception\ParserExceptionInterface; 3use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; 4 5try { 6 $parser->parse($source); 7} catch (SourceExceptionInterface $e) { 8 // The source could not be read at all 9} catch (LexerExceptionInterface $e) { 10 // The text could not be turned into tokens 11} catch (ParserExceptionInterface $e) { 12 // The tokens did not match the grammar 13}
Each has a RuntimeExceptionInterface counterpart for errors that happened
while reading a particular source, as opposed to errors in the setup:
1use Phplrt\Contracts\Parser\Exception\RuntimeExceptionInterface; 2 3catch (RuntimeExceptionInterface $e) { 4 // Something in the input, not something in the grammar 5}
Errors Of Your Own
Parsing is rarely the last step. The stages after it - resolving names, type
checking, evaluating - find their own problems, and those deserve the same
treatment. ErrorPrinter renders any offset in any source:
1use Phplrt\Exception\ErrorPrinter; 2use Phplrt\Source\VirtualFile; 3 4$source = new VirtualFile('config.txt', <<<'TXT' 5 name = "phplrt" 6 version = four 7 debug = true 8 TXT); 9 10echo new ErrorPrinter() 11 ->print($source, offset: 26, length: 4) 12 ->withMessage('Expected a number') 13 ->withClass('TypeError');
1error[TypeError]: Expected a number 2 --> config.txt:2:11 3 | 41 | name = "phplrt" 52 | version = four 6 | ^^^^ 73 | debug = true
This is where the $offset you kept on every AST node pays for itself.
Severity
1use Phplrt\Exception\Printer\Level; 2 3echo new ErrorPrinter() 4 ->print($source, 26, 4) 5 ->withMessage('Consider writing 4 instead') 6 ->withLevel(Level::Warning);
1warning: Consider writing 4 instead 2 --> config.txt:2:11 3 | 41 | name = "phplrt" 52 | version = four 6 | ^^^^ 73 | debug = true
Level::Error, Level::Warning and Level::Debug are available.
Adjusting The Output
Everything is a with*() method returning a new object, and the source is
read only when the result is turned into a string:
1$printer->print($source, $offset, $length) 2 ->withMessage('...') // the message above the snippet 3 ->withClass('MyError') // the name in brackets after the level 4 ->withLevel(Level::Warning) 5 ->withPathname('other.txt') // override the file name 6 ->withLinesAround(0) // no context lines, just the one that matters 7 ->withLength(4); // the size of the underlined fragment
withLinesAround(0) is worth knowing about - for a list of many warnings,
two context lines each is a wall of text.
Colors
Output to a terminal is colored, and output to anything else is not. Override that when you need to:
1use Phplrt\Exception\Printer\RustStylePrinter; 2 3$printer = new ErrorPrinter(new RustStylePrinter( 4 colors: false, 5)); 6 7// ...or set the width, for a narrow terminal 8$printer = new ErrorPrinter(new RustStylePrinter( 9 width: 80, 10));
A Different Format
PrinterInterface takes the captured lines and returns a string, so a
one-line-per-error format for a CI log is a small class:
1use Phplrt\Exception\Printer\ErrorInfo; 2use Phplrt\Exception\Printer\PrinterInterface; 3use Phplrt\Exception\Snippet\CapturedSourceLine; 4 5final class CompactPrinter implements PrinterInterface 6{ 7 public function print(iterable $snippets, ?ErrorInfo $info = null): string 8 { 9 foreach ($snippets as $line) { 10 // Only the lines containing the error itself are captured 11 if ($line instanceof CapturedSourceLine) { 12 return \sprintf( 13 '%s:%d:%d: %s', 14 $info?->pathname ?? '-', 15 $line->number, 16 $line->startColumn, 17 $info?->message ?? '', 18 ); 19 } 20 } 21 22 return $info?->message ?? ''; 23 } 24}
1$printer = new ErrorPrinter(new CompactPrinter()); 2 3echo $printer->print($source, 26, 4) 4 ->withMessage('Expected a number'); 5// config.txt:2:11: Expected a number
Attaching Snippets To Your Own Exceptions
The pattern the parser uses works for anything: keep the source and the
offset on the exception, and render them in __toString().
1use Phplrt\Contracts\Source\ReadableInterface; 2use Phplrt\Exception\ErrorPrinter; 3 4final class TypeException extends \RuntimeException 5{ 6 public function __construct( 7 string $message, 8 public readonly ReadableInterface $source, 9 public readonly int $offset, 10 public readonly int $length = 0, 11 ) { 12 parent::__construct($message); 13 } 14 15 public function __toString(): string 16 { 17 return (string) new ErrorPrinter() 18 ->print($this->source, $this->offset, $this->length) 19 ->withMessage($this->getMessage()) 20 ->withClass(static::class); 21 } 22}
Now every error your language reports looks the same as every error phplrt reports, which is exactly what you want.