Обработка ошибок в ANTLR4
Поведение по умолчанию, когда парсер не знает, что делать, - это печатать сообщения на терминале, например:
строка 1:23 отсутствует DECIMAL at '}'
Это хорошее сообщение, но не в том месте. Я предпочел бы получить это как исключение.
Я пробовал использовать BailErrorStrategy
, но это вызывает ParseCancellationException
без сообщения (вызванного InputMismatchException
, также без сообщения).
Есть ли способ заставить его сообщать об ошибках через исключения, сохраняя при этом полезную информацию в сообщении?
Вот что мне действительно нужно - я обычно использую действия в правилах для создания объекта:
dataspec returns [DataExtractor extractor]
@init {
DataExtractorBuilder builder = new DataExtractorBuilder(layout);
}
@after {
$extractor = builder.create();
}
: first=expr { builder.addAll($first.values); } (COMMA next=expr { builder.addAll($next.values); })* EOF
;
expr returns [List<ValueExtractor> values]
: a=atom { $values = Arrays.asList($a.val); }
| fields=fieldrange { $values = values($fields.fields); }
| '%' { $values = null; }
| ASTERISK { $values = values(layout); }
;
Затем, когда я вызываю парсер, я делаю что-то вроде этого:
public static DataExtractor create(String dataspec) {
CharStream stream = new ANTLRInputStream(dataspec);
DataSpecificationLexer lexer = new DataSpecificationLexer(stream);
CommonTokenStream tokens = new CommonTokenStream(lexer);
DataSpecificationParser parser = new DataSpecificationParser(tokens);
return parser.dataspec().extractor;
}
Все, что я действительно хочу, это
- для вызова
dataspec()
для генерирования исключения (в идеале проверенного), когда вход не может быть проанализирован
- для этого исключения есть полезное сообщение и предоставить доступ к номеру строки и положению, в котором была обнаружена проблема.
Затем я позволю этому исключению размыть столбец, чтобы он был лучше всего подходит для предоставления полезного сообщения пользователю - таким же образом я бы обработал сброшенное сетевое соединение, прочитал поврежденный файл и т.д.
Я видел, что в ANTLR4 теперь считается "продвинутым", так что, возможно, я странствую, но я не изучил, что "непереработанный" способ сделать это будет так как этот способ хорошо работает для наших нужд.
Ответы
Ответ 1
Поскольку у меня была небольшая борьба с двумя существующими ответами, я хотел бы поделиться решением, в котором я оказался.
Прежде всего, я создал свою собственную версию ErrorListener, например Сэм Харвелл:
public class ThrowingErrorListener extends BaseErrorListener {
public static final ThrowingErrorListener INSTANCE = new ThrowingErrorListener();
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e)
throws ParseCancellationException {
throw new ParseCancellationException("line " + line + ":" + charPositionInLine + " " + msg);
}
}
Обратите внимание на использование ParseCancellationException
вместо RecognitionException
, так как DefaultErrorStrategy поймает последнее и никогда не достигнет вашего собственного кода.
Создание целой новой ErrorStrategy, такой как Brad Mace, не требуется, поскольку DefaultErrorStrategy по умолчанию выводит довольно хорошие сообщения об ошибках.
Затем я использую пользовательский ErrorListener в моей функции синтаксического анализа:
public static String parse(String text) throws ParseCancellationException {
MyLexer lexer = new MyLexer(new ANTLRInputStream(text));
lexer.removeErrorListeners();
lexer.addErrorListener(ThrowingErrorListener.INSTANCE);
CommonTokenStream tokens = new CommonTokenStream(lexer);
MyParser parser = new MyParser(tokens);
parser.removeErrorListeners();
parser.addErrorListener(ThrowingErrorListener.INSTANCE);
ParserRuleContext tree = parser.expr();
MyParseRules extractor = new MyParseRules();
return extractor.visit(tree);
}
(Для получения дополнительной информации о том, что делает MyParseRules
, см. здесь.)
Это даст вам те же сообщения об ошибках, которые будут напечатаны на консоли по умолчанию, только в виде правильных исключений.
Ответ 2
Когда вы используете DefaultErrorStrategy
или BailErrorStrategy
, поле ParserRuleContext.exception
установлено для любого дерева синтаксиса node в полученном дереве разбора, где произошла ошибка. Документация для этого поля читает (для людей, которые не хотят нажимать дополнительную ссылку):
Исключение, заставившее это правило вернуться. Если правило успешно завершено, это null
.
Изменить: Если вы используете DefaultErrorStrategy
, исключение контекста синтаксиса не будет распространяться на весь код вызова, поэтому вы сможете напрямую изучить поле exception
, Если вы используете BailErrorStrategy
, бросок ParseCancellationException
, который он выбрал, будет содержать RecognitionException
, если вы вызываете getCause()
.
if (pce.getCause() instanceof RecognitionException) {
RecognitionException re = (RecognitionException)pce.getCause();
ParserRuleContext context = (ParserRuleContext)re.getCtx();
}
Изменить 2:. На основе вашего другого ответа кажется, что вы действительно не хотите исключение, но то, что вы хотите, - это другой способ сообщить об ошибках. В этом случае вас больше интересует интерфейс ANTLRErrorListener
. Вы хотите вызвать parser.removeErrorListeners()
, чтобы удалить прослушиватель по умолчанию, который записывается на консоль, а затем вызывается parser.addErrorListener(listener)
для вашего собственного специального слушателя. Я часто использую следующий слушатель в качестве отправной точки, так как он включает имя исходного файла с сообщениями.
public class DescriptiveErrorListener extends BaseErrorListener {
public static DescriptiveErrorListener INSTANCE = new DescriptiveErrorListener();
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol,
int line, int charPositionInLine,
String msg, RecognitionException e)
{
if (!REPORT_SYNTAX_ERRORS) {
return;
}
String sourceName = recognizer.getInputStream().getSourceName();
if (!sourceName.isEmpty()) {
sourceName = String.format("%s:%d:%d: ", sourceName, line, charPositionInLine);
}
System.err.println(sourceName+"line "+line+":"+charPositionInLine+" "+msg);
}
}
Если этот класс доступен, вы можете использовать его для использования.
lexer.removeErrorListeners();
lexer.addErrorListener(DescriptiveErrorListener.INSTANCE);
parser.removeErrorListeners();
parser.addErrorListener(DescriptiveErrorListener.INSTANCE);
A много более сложный пример прослушивателя ошибок, который я использую для идентификации двусмысленностей, которые делают грамматику без SLL, SummarizingDiagnosticErrorListener
class в TestPerformance
.
Ответ 3
То, что я придумал до сих пор, основано на расширении DefaultErrorStrategy
и переопределении его методов reportXXX
(хотя вполне возможно, что я делаю вещи более сложными, чем необходимо):
public class ExceptionErrorStrategy extends DefaultErrorStrategy {
@Override
public void recover(Parser recognizer, RecognitionException e) {
throw e;
}
@Override
public void reportInputMismatch(Parser recognizer, InputMismatchException e) throws RecognitionException {
String msg = "mismatched input " + getTokenErrorDisplay(e.getOffendingToken());
msg += " expecting one of "+e.getExpectedTokens().toString(recognizer.getTokenNames());
RecognitionException ex = new RecognitionException(msg, recognizer, recognizer.getInputStream(), recognizer.getContext());
ex.initCause(e);
throw ex;
}
@Override
public void reportMissingToken(Parser recognizer) {
beginErrorCondition(recognizer);
Token t = recognizer.getCurrentToken();
IntervalSet expecting = getExpectedTokens(recognizer);
String msg = "missing "+expecting.toString(recognizer.getTokenNames()) + " at " + getTokenErrorDisplay(t);
throw new RecognitionException(msg, recognizer, recognizer.getInputStream(), recognizer.getContext());
}
}
Это генерирует исключения с полезными сообщениями, а строка и положение проблемы могут быть получены либо из токена offending
, либо если который не установлен, из current
токена, используя ((Parser) re.getRecognizer()).getCurrentToken()
в RecognitionException
.
Я довольно доволен тем, как это работает, хотя использование шести методов reportX
для переопределения заставляет меня думать, что лучший способ.