97 votes

Gestion des erreurs dans ANTLR4

Le comportement par défaut lorsque l'analyseur ne sait pas quoi faire est d'imprimer des messages dans le terminal comme :

ligne 1:23 : DECIMAL manquant à '}'.

C'est un bon message, mais au mauvais endroit. Je préférerais le recevoir comme une exception.

J'ai essayé d'utiliser le BailErrorStrategy mais cela déclenche un ParseCancellationException sans message (causé par un InputMismatchException (également sans message).

Existe-t-il un moyen de faire en sorte que les erreurs soient signalées par des exceptions tout en conservant les informations utiles dans le message ?


Voici ce que je cherche vraiment : j'utilise généralement des actions dans les règles pour construire un objet :

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

Ensuite, lorsque j'invoque le parseur, je fais quelque chose comme ceci :

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

Tout ce que je veux vraiment, c'est

  • pour le dataspec() appel pour lancer une exception (idéalement une exception vérifiée) lorsque l'entrée ne peut pas être analysée.
  • pour que cette exception ait un message utile et donne accès au numéro de ligne et à la position où le problème a été trouvé

Ensuite, je laisserai cette exception remonter la pile d'appels jusqu'à l'endroit le plus approprié pour présenter un message utile à l'utilisateur - de la même manière que je traiterais une connexion réseau interrompue, la lecture d'un fichier corrompu, etc.

J'ai vu que les actions sont maintenant considérées comme "avancées" dans ANTLR4, alors peut-être que je m'y prends d'une manière étrange, mais je n'ai pas cherché à savoir quelle serait la manière "non avancée" de procéder puisque cette méthode a bien fonctionné pour nos besoins.

116voto

Mouagip Points 521

Comme j'ai eu un peu de mal avec les deux réponses existantes, j'aimerais partager la solution à laquelle j'ai abouti.

Tout d'abord, j'ai créé ma propre version d'un ErrorListener comme suit Sam Harwell suggéré :

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

Notez l'utilisation d'un ParseCancellationException au lieu d'un RecognitionException car la DefaultErrorStrategy attraperait cette dernière et elle n'atteindrait jamais votre propre code.

Créer une toute nouvelle ErrorStrategy comme Brad Mace suggéré n'est pas nécessaire puisque la stratégie DefaultErrorStrategy produit par défaut d'assez bons messages d'erreur.

J'utilise ensuite l'écouteur d'erreur personnalisé dans ma fonction d'analyse syntaxique :

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

(Pour plus d'informations sur ce que MyParseRules fait, voir ici .)

Vous obtiendrez ainsi les mêmes messages d'erreur que ceux qui seraient imprimés dans la console par défaut, mais sous la forme d'exceptions appropriées.

55voto

280Z28 Points 49515

Lorsque vous utilisez le DefaultErrorStrategy ou le BailErrorStrategy le ParserRuleContext.exception est défini pour tout nœud de l'arbre d'analyse dans l'arbre d'analyse résultant où une erreur s'est produite. La documentation pour ce champ est la suivante (pour les personnes qui ne veulent pas cliquer sur un lien supplémentaire) :

L'exception qui a forcé le retour de cette règle. Si la règle s'est déroulée avec succès, c'est null .

Edita: Si vous utilisez DefaultErrorStrategy l'exception relative au contexte d'analyse ne sera pas propagée jusqu'au code appelant, ce qui vous permettra d'examiner le code d'erreur de l'utilisateur. exception directement sur le terrain. Si vous utilisez BailErrorStrategy le ParseCancellationException qu'il lance comprendra un RecognitionException si vous appelez getCause() .

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

Edit 2 : D'après votre autre réponse, il semble que vous ne vouliez pas vraiment d'exception, mais que vous souhaitiez une manière différente de signaler les erreurs. Dans ce cas, vous serez plus intéressé par la fonction ANTLRErrorListener interface. Vous voulez appeler [parser.removeErrorListeners()](http://www.antlr.org/api/Java/org/antlr/v4/runtime/Recognizer.html#removeErrorListeners()) pour supprimer l'écouteur par défaut qui écrit dans la console, et ensuite appeler parser.addErrorListener(listener) pour votre auditeur particulier. J'utilise souvent le listener suivant comme point de départ, car il inclut le nom du fichier source avec les messages.

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

Cette classe étant disponible, vous pouvez utiliser les éléments suivants pour l'utiliser.

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

A mucho L'exemple le plus compliqué d'un détecteur d'erreurs que j'utilise pour identifier les ambiguïtés qui rendent une grammaire non-SLL est la fonction SummarizingDiagnosticErrorListener classe dans TestPerformance .

10voto

Brad Mace Points 12173

Ce que j'ai trouvé jusqu'à présent est basé sur l'extension de l'utilisation de l'ordinateur. DefaultErrorStrategy et de remplacer son reportXXX (bien qu'il soit tout à fait possible que je rende les choses plus compliquées que nécessaire) :

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

Cela génère des exceptions avec des messages utiles, et la ligne et la position du problème peuvent être obtenues à partir de l'un ou l'autre des éléments suivants [offending](http://antlr4.org/api/Java/org/antlr/v4/runtime/RecognitionException.html#getOffendingToken()) ou, s'il n'est pas défini, de l'indicateur [current](http://antlr4.org/api/Java/org/antlr/v4/runtime/Parser.html#getCurrentToken()) en utilisant ((Parser) re.getRecognizer()).getCurrentToken() sur le RecognitionException .

Je suis assez satisfait de la façon dont ça fonctionne, même si le fait d'avoir six reportX des méthodes à remplacer me fait penser qu'il y a un meilleur moyen.

1voto

geekley Points 545

Pour ceux que cela intéresse, voici l'équivalent ANTLR4 C# de la réponse de Sam Harwell :

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

1voto

charlee Points 828

Pour les personnes qui utilisent Python, voici la solution en Python 3 basée sur La réponse de Mouagip .

Tout d'abord, définissez un écouteur d'erreur personnalisé :

from antlr4.error.ErrorListener import ErrorListener
from antlr4.error.Errors import ParseCancellationException

class ThrowingErrorListener(ErrorListener):
    def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
        ex = ParseCancellationException(f'line {line}: {column} {msg}')
        ex.line = line
        ex.column = column
        raise ex

Ensuite, définissez ceci sur lexer et parser :

lexer = MyScriptLexer(script)
lexer.removeErrorListeners()
lexer.addErrorListener(ThrowingErrorListener())

token_stream = CommonTokenStream(lexer)

parser = MyScriptParser(token_stream)
parser.removeErrorListeners()
parser.addErrorListener(ThrowingErrorListener())

tree = parser.script()

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X