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

83

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

строка 1:23 отсутствует DECIMAL в '}'

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

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

Брэд Мейс
источник

Ответы:

98

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

Прежде всего, я создал свою собственную версию 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);
      }
}

Обратите внимание на использование a ParseCancellationExceptionвместо a, 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, что делает, см. Здесь .)

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

Муагип
источник
3
Я пробовал это и подтверждаю, что это сработало. Думаю, это самое простое из 3 предложенных решений.
Ками
1
Это правильный путь. Самый простой способ пойти. «Проблема» возникает в лексере, и имеет смысл сразу же сообщить о ней, если важно, чтобы ввод был действительным, прежде чем пытаться выполнить синтаксический анализ. ++
RubberDuck
Есть ли особая причина использовать этот ThrowingErrorListenerкласс как синглтон?
RonyHe
@RonyHe Нет, это всего лишь адаптация кода Сэма Харвелла .
Mouagip
Это решение сработало для меня с одним предостережением - мы пытаемся выполнить синтаксический анализ с использованием SLL, а затем возвращаемся к LL, и оказалось, что это не привело к возникновению ошибки при выполнении резервного синтаксического анализа. Обходной путь заключался в создании совершенно нового парсера для второй попытки вместо сброса парсера - очевидно, сброс парсера не приводит к сбросу какого-то важного состояния.
Trejkaz
51

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

Исключение, которое заставило вернуть это правило. Если правило успешно выполнено, то это 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);

Гораздо более сложный пример слушателя ошибки , которые я использую , чтобы определить двусмысленности , которые делают грамматику , не SLL является SummarizingDiagnosticErrorListenerклассTestPerformance .

Сэм Харвелл
источник
Хорошо ... как мне это использовать? Могу ли я использовать что-то вроде ((InputMismatchException) pce.getCause()).getCtx().exceptionполезного сообщения об ошибке?
Брэд Мейс,
1
Я немного поэкспериментировал с выдачей исключения из прослушивателя ошибок, но, похоже, исключение никогда не появляется. Я только что закончил с NPE от действий в грамматике из-за неудачных совпадений. Я добавил предысторию к вопросу, поскольку, похоже, я плыву против течения.
Брэд Мейс,
Вам просто нужно написать служебный класс, чтобы возвращать «строку», «столбец» и «сообщение» из файла RecognitionException. Необходимая информация доступна в уже созданном исключении.
Сэм Харвелл,
Любезный читатель, если вы похожи на меня, вам интересно, что такое REPORT_SYNTAX_ERRORS. Вот ответ: stackoverflow.com/questions/18581880/handling-errors-in-antlr-4
james.garriss
Этот пример действительно полезен. Я думаю, это должно быть где-то в официальной документации , похоже, не хватает страницы для обработки ошибок. По крайней мере, было бы хорошо упомянуть о слушателях ошибок.
geekley
10

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

Брэд Мейс
источник
работает лучше для C #, принятый и получивший наибольшее количество голосов ответ имел ошибки компиляции в C #, некоторая несовместимость аргумента generics IToken vs int
sarh 07
0

Для всех, кто интересуется, вот ANTLR4 C # эквивалент ответа Сэма Харвелла:

using System; using System.IO; using Antlr4.Runtime;
public class DescriptiveErrorListener : BaseErrorListener, IAntlrErrorListener<int>
{
  public static DescriptiveErrorListener Instance { get; } = new DescriptiveErrorListener();
  public void SyntaxError(TextWriter output, IRecognizer recognizer, int offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) {
    if (!REPORT_SYNTAX_ERRORS) return;
    string sourceName = recognizer.InputStream.SourceName;
    // never ""; might be "<unknown>" == IntStreamConstants.UnknownSourceName
    sourceName = $"{sourceName}:{line}:{charPositionInLine}";
    Console.Error.WriteLine($"{sourceName}: line {line}:{charPositionInLine} {msg}");
  }
  public override void SyntaxError(TextWriter output, IRecognizer recognizer, Token offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) {
    this.SyntaxError(output, recognizer, 0, line, charPositionInLine, msg, e);
  }
  static readonly bool REPORT_SYNTAX_ERRORS = true;
}
lexer.RemoveErrorListeners();
lexer.AddErrorListener(DescriptiveErrorListener.Instance);
parser.RemoveErrorListeners();
parser.AddErrorListener(DescriptiveErrorListener.Instance);
гикли
источник