Skip to content

Latest commit

History

History
270 lines (212 loc) · 6.82 KB

File metadata and controls

270 lines (212 loc) · 6.82 KB

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:

error[UnexpectedTokenException]: Syntax error, unexpected "3" (T_NUMBER), T_PLUS expected
--> expr.txt:2:1
|
1 | 1 + 2
2 | 3 * (4 + )
| ^
3 |

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

usePhplrt\Parser\Exception\UnexpectedTokenException;
usePhplrt\Source\VirtualFile;
try {
$parser->parse(newVirtualFile('expr.txt', $input));
} catch (UnexpectedTokenException$e) {
echo$e->getMessage(); // Syntax error, unexpected "3" (T_NUMBER), T_PLUS expectedecho$e; // ...plus the snippet above
}

The exception carries everything needed to build your own message:

$e->token; // the token it choked on$e->token->name; // T_NUMBER$e->token->offset; // 6$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(newSource($input));
error[UnexpectedTokenException]: Syntax error, unexpected "3" (T_NUMBER), T_PLUS expected
|
1 | 3
| ^

Wrap the input in a File or a VirtualFile and the error can say where:

$parser->parse(newVirtualFile('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:

usePhplrt\Contracts\Lexer\Exception\LexerExceptionInterface;
usePhplrt\Contracts\Parser\Exception\ParserExceptionInterface;
usePhplrt\Contracts\Source\Exception\SourceExceptionInterface;
try {
$parser->parse($source);
} catch (SourceExceptionInterface$e) {
// The source could not be read at all
} catch (LexerExceptionInterface$e) {
// The text could not be turned into tokens
} catch (ParserExceptionInterface$e) {
// The tokens did not match the grammar
}

Each has a RuntimeExceptionInterface counterpart for errors that happened while reading a particular source, as opposed to errors in the setup:

usePhplrt\Contracts\Parser\Exception\RuntimeExceptionInterface;
catch (RuntimeExceptionInterface $e) {
// Something in the input, not something in the grammar
}

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:

usePhplrt\Exception\ErrorPrinter;
usePhplrt\Source\VirtualFile;
$source = newVirtualFile('config.txt', <<<'TXT' name = "phplrt" version = four debug = true
TXT);
echonewErrorPrinter()
->print($source, offset: 26, length: 4)
->withMessage('Expected a number')
->withClass('TypeError');
error[TypeError]: Expected a number
--> config.txt:2:11
|
1 | name = "phplrt"
2 | version = four
| ^^^^
3 | debug = true

This is where the $offset you kept on every AST node pays for itself.

Severity

usePhplrt\Exception\Printer\Level;
echonewErrorPrinter()
->print($source, 26, 4)
->withMessage('Consider writing 4 instead')
->withLevel(Level::Warning);
warning: Consider writing 4 instead
--> config.txt:2:11
|
1 | name = "phplrt"
2 | version = four
| ^^^^
3 | 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:

$printer->print($source, $offset, $length)
->withMessage('...') // the message above the snippet
->withClass('MyError') // the name in brackets after the level
->withLevel(Level::Warning)
->withPathname('other.txt') // override the file name
->withLinesAround(0) // no context lines, just the one that matters
->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:

usePhplrt\Exception\Printer\RustStylePrinter;
$printer = newErrorPrinter(newRustStylePrinter(
colors: false,
));
// ...or set the width, for a narrow terminal$printer = newErrorPrinter(newRustStylePrinter(
width: 80,
));

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:

usePhplrt\Exception\Printer\ErrorInfo;
usePhplrt\Exception\Printer\PrinterInterface;
usePhplrt\Exception\Snippet\CapturedSourceLine;
finalclass CompactPrinter implements PrinterInterface
{
publicfunctionprint(iterable$snippets, ?ErrorInfo$info = null): string
{
foreach ($snippetsas$line) {
// Only the lines containing the error itself are capturedif ($lineinstanceof CapturedSourceLine) {
return\sprintf(
'%s:%d:%d: %s',
$info?->pathname ?? '-',
$line->number,
$line->startColumn,
$info?->message ?? '',
);
}
}
return$info?->message ?? '';
}
}
$printer = newErrorPrinter(newCompactPrinter());
echo$printer->print($source, 26, 4)
->withMessage('Expected a number');
// 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().

usePhplrt\Contracts\Source\ReadableInterface;
usePhplrt\Exception\ErrorPrinter;
finalclass TypeException extends \RuntimeException
{
publicfunction__construct(
string$message,
publicreadonlyReadableInterface$source,
publicreadonlyint$offset,
publicreadonlyint$length = 0,
) {
parent::__construct($message);
}
publicfunction__toString(): string
{
return (string) newErrorPrinter()
->print($this->source, $this->offset, $this->length)
->withMessage($this->getMessage())
->withClass(static::class);
}
}

Now every error your language reports looks the same as every error phplrt reports, which is exactly what you want.