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

Отменить ошибку разбора с полезным сообщением

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

Вот что я имею в данный момент:

grammar Toy;

@parser::members {

    public static void main(String[] args) {
        for (String arg: args)
            System.out.println(arg + " => " + parse(arg));
    }

    public static String parse(String code) {
        ErrorListener errorListener = new ErrorListener();
        CharStream cstream = new ANTLRInputStream(code);
        ToyLexer lexer = new ToyLexer(cstream);
        lexer.removeErrorListeners();
        lexer.addErrorListener(errorListener);
        TokenStream tstream = new CommonTokenStream(lexer);
        ToyParser parser = new ToyParser(tstream);
        parser.removeErrorListeners();
        parser.addErrorListener(errorListener);
        parser.setErrorHandler(new BailErrorStrategy());
        try {
            String res = parser.top().str;
            if (errorListener.message != null)
                return "Parsed, but " + errorListener.toString();
            return res;
        } catch (ParseCancellationException e) {
            if (errorListener.message != null)
                return "Failed, because " + errorListener.toString();
            throw e;
        }
    }

    static class ErrorListener extends BaseErrorListener {

        String message = null;
        int start = -2, stop = -2, line = -2;

        @Override
        public void syntaxError(Recognizer<?, ?> recognizer,
                                Object offendingSymbol,
                                int line,
                                int charPositionInLine,
                                String msg,
                                RecognitionException e) {
            if (message != null) return;
            if (offendingSymbol instanceof Token) {
                Token t = (Token) offendingSymbol;
                start = t.getStartIndex();
                stop = t.getStopIndex();
            } else if (recognizer instanceof ToyLexer) {
                ToyLexer lexer = (ToyLexer)recognizer;
                start = lexer._tokenStartCharIndex;
                stop = lexer._input.index();
            }
            this.line = line;
            message = msg;
        }

        @Override public String toString() {
            return start + "-" + stop + " l." + line + ": " + message;
        }
    }

}

top returns [String str]: e* EOF {$str = "All went well.";};
e: 'a' 'b' | 'a' 'c' e;

Сохраните это значение в Toy.g, затем выполните следующие команды:

> java -jar antlr-4.5.2-complete.jar Toy.g
> javac -cp antlr-4.5.2-complete.jar Toy*.java
> java -cp .:tools/antlr-4.5.2-complete.jar ToyParser ab acab acc axb abc
ab => All went well.
acab => All went well.
acc => Failed, because 2-2 l.1: no viable alternative at input 'c'
axb => Parsed, but 1-1 l.1: token recognition error at: 'x'
Exception in thread "main" org.antlr.v4.runtime.misc.ParseCancellationException
    at org.antlr.v4.runtime.BailErrorStrategy.recoverInline(BailErrorStrategy.java:90)
    at org.antlr.v4.runtime.Parser.match(Parser.java:229)
    at ToyParser.top(ToyParser.java:187)
    at ToyParser.parse(ToyParser.java:95)
    at ToyParser.main(ToyParser.java:80)
Caused by: org.antlr.v4.runtime.InputMismatchException
    at org.antlr.v4.runtime.BailErrorStrategy.recoverInline(BailErrorStrategy.java:85)
    ... 4 more

С одной стороны, я чувствую, что я уже слишком много делаю. Глядя на то, сколько кода я написал для того, что должно быть простой и общей задачей, я не могу не задаться вопросом, не хватает ли я более простого решения. С другой стороны, даже этого недостаточно, по двум причинам. Во-первых, хотя мне удалось получить сообщение об ошибке lexer, они все равно не мешают продолжению анализатора на оставшемся потоке. Это свидетельствует строка Parsed, but для ввода axb. Во-вторых, я все еще остаюсь с ошибками, которые не сообщаются в прослушиватель ошибок, о чем свидетельствует трассировка стека.

Если я не устанавливаю BailErrorStrategy, я получаю более полезный вывод:

acc => Parsed, but 2-2 l.1: mismatched input 'c' expecting 'a'
axb => Parsed, but 1-1 l.1: token recognition error at: 'x'
abc => Parsed, but 2-2 l.1: extraneous input 'c' expecting {<EOF>, 'a'}

Есть ли способ получить подобные сообщения об ошибках, но все же залог при ошибке? Я могу видеть из источников, что сообщение extraneous input действительно генерируется DefaultErrorStrategy, по-видимому, после того, как оно разработало вопрос об устранении проблемы. Должен ли я позволить этому сделать это, а затем выручить, то есть написать свой собственный вариант BailErrorStrategy, который вызывает супер перед бросанием?

4b9b3361

Ответ 1

В той же ситуации я закончил с расширением DefaultErrorStrategy и переопределением методов report*. Это довольно просто (вы можете использовать ANTLRErrorStrategy).

Здесь вы можете найти пример отказоустойчивой стратегии. Я думаю, что в вашем случае вы можете собрать все ошибки таким же образом и построить подробный отчет.

Ответ 2

Один подход может изменить регистратор ошибок вместо стратегии ошибки. Можно использовать стратегию по умолчанию вместе со следующим слушателем:

class ErrorListener extends BaseErrorListener {
    @Override
    public void syntaxError(Recognizer<?, ?> recognizer,
                            Object offendingSymbol,
                            int line,
                            int charPositionInLine,
                            String msg,
                            RecognitionException e) {
        throw new ParseException(msg, e, line);
    }
}

class ParseException extends RuntimeException {
    int line;
    public ParseException(String message, Throwable cause, int line) {
        super(message, cause);
        this.line = line;
    }
}

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

Что касается машиночитаемого местоположения, если в дополнение к номеру строки вам также нужны смещения исходного текста для нарушающей части ввода, такой код, похоже, работает внутри метода syntaxError:

        int start = 0, stop = -1;
        if (offendingSymbol instanceof Token) {
            Token t = (Token) offendingSymbol;
            start = t.getStartIndex();
            stop = t.getStopIndex();
        } else if (recognizer instanceof Lexer) {
            Lexer lexer = (Lexer)recognizer;
            start = lexer._tokenStartCharIndex;
            stop = lexer._input.index();
        }