Обработка ошибок в ANTLR4

поведение по умолчанию, когда парсер не знает, что делать, это печатать сообщения на терминал, как:

строка 1: 23 отсутствует десятичная в '}'

это хорошее сообщение, но не в том месте. Я бы предпочел принять это как исключение.

Я пробовал использовать BailErrorStrategy, но это бросает!--3--> без сообщения (вызвано 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() вызов для создания исключения (в идеале проверенного), когда вход не может быть проанализирован
  • для этого исключения, чтобы иметь полезное сообщение и обеспечить доступ к номеру строки и позиции, где была найдена проблема

тогда я позволю этому исключению пузыриться в callstack туда, где лучше всего подходит для представления полезного сообщения пользователю-так же, как я бы обрабатывал удаленное сетевое соединение, чтение поврежденного файла и т. д.

Я видел, что действия теперь считаются "продвинутыми" в ANTLR4, поэтому, возможно, я иду о вещах странным образом, но я не изучал, что такое "не продвинутый" способ сделать это было бы, так как этот способ хорошо работает для наших нужд.

3 ответов


поскольку у меня была небольшая борьба с двумя существующими ответами, я хотел бы поделиться решением, с которым я закончил.

прежде всего, я создал свою собственную версию 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 как бред Мейс!--9--> suggested не требуется, так как 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 не вижу здесь.)

Это даст вам те же сообщения об ошибках, которые были бы напечатаны на консоли по умолчанию, только в виде надлежащих исключений.


при использовании DefaultErrorStrategy или BailErrorStrategy на ParserRuleContext.exception поле задается для любого узла дерева синтаксического анализа в результирующем дереве синтаксического анализа, где произошла ошибка. Документация для этого поля гласит (для людей, которые не хотят нажимать дополнительную ссылку):

исключение, которое заставило это правило вернуться. Если правило успешно завершено, это null.

Edit: если вы использовать DefaultErrorStrategy, исключение контекста синтаксического анализа не будет распространяться полностью на вызывающий код, поэтому вы сможете изучить exception поле напрямую. Если вы используете BailErrorStrategy, the ParseCancellationException брошенный им будет включать в себя RecognitionException если вы называете getCause().

if (pce.getCause() instanceof RecognitionException) {
    RecognitionException re = (RecognitionException)pce.getCause();
    ParserRuleContext context = (ParserRuleContext)re.getCtx();
}

Edit 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 класс TestPerformance.


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