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

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

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

3 53

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