Behandlung von Fehlern in ANTLR4

80

Das Standardverhalten, wenn der Parser nicht weiß, was zu tun ist, besteht darin, Nachrichten wie folgt an das Terminal zu drucken:

Zeile 1:23 fehlt DECIMAL bei '}'

Dies ist eine gute Nachricht, aber am falschen Ort. Ich möchte dies lieber als Ausnahme erhalten.

Ich habe versucht, die zu verwenden BailErrorStrategy, aber dies wirft eine ParseCancellationExceptionohne Nachricht (verursacht durch eine InputMismatchException, auch ohne Nachricht).

Gibt es eine Möglichkeit, Fehler über Ausnahmen zu melden, während die nützlichen Informationen in der Nachricht erhalten bleiben?


Folgendes ist mir wirklich wichtig: Normalerweise verwende ich Aktionen in Regeln, um ein Objekt aufzubauen:

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); }
    ;

Wenn ich dann den Parser aufrufe, mache ich so etwas:

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;
}

Alles was ich wirklich will ist

  • Damit der dataspec()Aufruf eine Ausnahme auslöst (idealerweise eine aktivierte), wenn die Eingabe nicht analysiert werden kann
  • Damit diese Ausnahme eine nützliche Nachricht enthält und Zugriff auf die Zeilennummer und die Position bietet, an der das Problem gefunden wurde

Dann lasse ich diese Ausnahme den Callstack dahin sprudeln, wo es am besten geeignet ist, dem Benutzer eine nützliche Nachricht zu präsentieren - genauso wie ich mit einer unterbrochenen Netzwerkverbindung, dem Lesen einer beschädigten Datei usw. umgehen würde.

Ich habe gesehen, dass Aktionen in ANTLR4 jetzt als "fortgeschritten" angesehen werden, also gehe ich die Dinge vielleicht auf seltsame Weise an, aber ich habe nicht untersucht, wie die "nicht fortgeschrittene" Art, dies zu tun, seit dieser Weise aussehen würde hat gut für unsere Bedürfnisse gearbeitet.

Brad Mace
quelle

Antworten:

94

Da ich mit den beiden vorhandenen Antworten ein wenig zu kämpfen hatte, möchte ich die Lösung, mit der ich am Ende endete, teilen.

Zuerst habe ich meine eigene Version eines ErrorListener erstellt, wie Sam Harwell vorgeschlagen hat:

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);
      }
}

Beachten Sie die Verwendung von a ParseCancellationExceptionanstelle von a, RecognitionExceptionda die DefaultErrorStrategy letztere abfangen und niemals Ihren eigenen Code erreichen würde.

Das Erstellen einer ganz neuen ErrorStrategy, wie von Brad Mace vorgeschlagen, ist nicht erforderlich, da die DefaultErrorStrategy standardmäßig ziemlich gute Fehlermeldungen erzeugt.

Ich verwende dann den benutzerdefinierten ErrorListener in meiner Analysefunktion:

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);
}

(Weitere Informationen dazu MyParseRulesfinden Sie hier .)

Dies gibt Ihnen die gleichen Fehlermeldungen, die standardmäßig auf der Konsole gedruckt werden, nur in Form der richtigen Ausnahmen.

Mouagip
quelle
3
Ich habe es versucht und bestätige, dass es gut funktioniert hat. Ich denke, dies ist die einfachste der drei vorgeschlagenen Lösungen.
Kami
1
Dies ist der richtige Weg. Einfachster Weg. Das "Problem" tritt im Lexer auf und es ist sinnvoll, es sofort zu melden, wenn es wichtig ist, dass die Eingabe gültig ist, bevor versucht wird, sie zu analysieren. ++
RubberDuck
Gibt es einen bestimmten Grund, die ThrowingErrorListenerKlasse als Singleton zu verwenden?
RonyHe
@RonyHe Nein, dies ist nur eine Anpassung des Sam Harwells-Codes .
Mouagip
Diese Lösung hat bei mir mit einer Einschränkung funktioniert: Wir versuchen, mithilfe von SLL zu analysieren und dann auf LL zurückzugreifen. Dabei stellte sich heraus, dass beim Fallback-Parsing kein Fehler aufgetreten ist. Die Problemumgehung bestand darin, einen völlig neuen Parser für den zweiten Versuch zu erstellen, anstatt den Parser zurückzusetzen. Anscheinend kann das Zurücksetzen des Parsers einen wichtigen Status nicht zurücksetzen.
Trejkaz
50

Wenn Sie das DefaultErrorStrategyoder das verwenden BailErrorStrategy, wird das ParserRuleContext.exceptionFeld für jeden Analysebaumknoten im resultierenden Analysebaum festgelegt, bei dem ein Fehler aufgetreten ist. Die Dokumentation für dieses Feld lautet (für Personen, die nicht auf einen zusätzlichen Link klicken möchten):

Die Ausnahme, die diese Regel zur Rückkehr zwang. Wenn die Regel erfolgreich abgeschlossen wurde, ist dies null.

Bearbeiten: Wenn Sie verwenden DefaultErrorStrategy, wird die Analysekontextausnahme nicht bis zum aufrufenden Code weitergegeben, sodass Sie das exceptionFeld direkt untersuchen können. Wenn Sie verwenden BailErrorStrategy, enthält das ParseCancellationExceptionvon ihm geworfene ein, RecognitionExceptionwenn Sie anrufen getCause().

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

Bearbeiten 2: Basierend auf Ihrer anderen Antwort scheint es, dass Sie eigentlich keine Ausnahme wollen, aber was Sie wollen, ist eine andere Art, die Fehler zu melden. In diesem Fall interessieren Sie sich mehr für die ANTLRErrorListenerBenutzeroberfläche. Sie möchten aufrufen parser.removeErrorListeners(), um den Standard-Listener zu entfernen, der in die Konsole schreibt, und dann parser.addErrorListener(listener)Ihren eigenen speziellen Listener aufrufen . Ich verwende oft den folgenden Listener als Ausgangspunkt, da er den Namen der Quelldatei mit den Nachrichten enthält.

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);
    }
}

Wenn diese Klasse verfügbar ist, können Sie sie wie folgt verwenden.

lexer.removeErrorListeners();
lexer.addErrorListener(DescriptiveErrorListener.INSTANCE);
parser.removeErrorListeners();
parser.addErrorListener(DescriptiveErrorListener.INSTANCE);

Ein viel komplizierteres Beispiel für einen Fehler-Listener, mit dem ich Mehrdeutigkeiten identifiziere, die eine Grammatik nicht SLL machen, ist die SummarizingDiagnosticErrorListenerKlasse inTestPerformance .

Sam Harwell
quelle
Ok ... wie nutze ich das aber? Soll ich so etwas verwenden ((InputMismatchException) pce.getCause()).getCtx().exception, um an die nützliche Fehlermeldung zu gelangen?
Brad Mace
1
Ich habe ein wenig damit experimentiert, die Ausnahme vom Fehler-Listener auszulösen, aber die Ausnahme scheint nie aufzutauchen. Ich habe gerade NPEs von den Aktionen in der Grammatik aufgrund der fehlgeschlagenen Übereinstimmungen erhalten. Ich habe der Frage eine Hintergrundgeschichte hinzugefügt, da es den Anschein hat, als würde ich gegen den Strom schwimmen.
Brad Mace
Sie sollten einfach eine Dienstprogrammklasse schreiben, um die "Zeile", "Spalte" und "Nachricht" von a zurückzugeben RecognitionException. Die gewünschten Informationen sind in der Ausnahme verfügbar, die bereits ausgelöst wird.
Sam Harwell
Gentle Reader, wenn Sie wie ich sind, fragen Sie sich, worum es bei REPORT_SYNTAX_ERRORS geht. Hier ist die Antwort: stackoverflow.com/questions/18581880/handling-errors-in-antlr-4
james.garriss
10

Was ich bisher entwickelt habe, basiert auf der Erweiterung DefaultErrorStrategyund Überschreibung der reportXXXMethoden (obwohl es durchaus möglich ist, dass ich die Dinge komplizierter als nötig mache):

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());
    }
}

Dies löst Ausnahmen mit nützlichen Nachrichten aus, und die Zeile und Position des Problems kann entweder vom offendingToken oder, falls dies nicht festgelegt ist, vom currentToken mithilfe von ((Parser) re.getRecognizer()).getCurrentToken()auf dem Token abgerufen werden RecognitionException.

Ich bin ziemlich zufrieden mit der Funktionsweise, obwohl reportXich denke, dass es einen besseren Weg gibt , wenn ich sechs Methoden zum Überschreiben habe.

Brad Mace
quelle
funktioniert besser für c #, akzeptierte und am besten gewählte Antwort hatte Kompilierungsfehler in c #, einige Inkompatibilität des generischen Arguments IToken vs int
sarh