Подтвердить что ты не робот

Обработка ошибок в 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 теперь считается "продвинутым", так что, возможно, я странствую, но я не изучил, что "непереработанный" способ сделать это будет так как этот способ хорошо работает для наших нужд.

4b9b3361

Ответ 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 для переопределения заставляет меня думать, что лучший способ.