web-dev-qa-db-fra.com

Gestion des erreurs dans ANTLR4

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

ligne 1:23 manquante DECIMAL à '}'

C'est un bon message, mais au mauvais endroit. Je préfère recevoir cela à titre d'exception.

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

Existe-t-il un moyen de le faire signaler des erreurs via des exceptions tout en conservant les informations utiles dans le message?


Voici ce que je recherche vraiment - j'utilise généralement des actions dans les règles pour créer 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, quand j'invoque l'analyseur, 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 c'est

  • pour l'appel dataspec() pour lever une exception (idéalement 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é détecté

Ensuite, je laisserai cette exception remonter la pile d'appels à l'endroit qui convient le mieux pour présenter un message utile à l'utilisateur - de la même manière que je gérerais une connexion réseau interrompue, lisant 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 traite les choses d'une manière étrange, mais je n'ai pas examiné ce que serait la façon "non avancée" de le faire depuis cette façon a bien fonctionné pour nos besoins.

61
Brad Mace

Puisque j'ai eu un peu de mal avec les deux réponses existantes, je voudrais partager la solution avec laquelle je me suis retrouvé.

Tout d'abord, j'ai créé ma propre version d'un ErrorListener comme 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 le DefaultErrorStrategy intercepterait ce dernier et n'atteindrait jamais votre propre code.

La création d'une toute nouvelle ErrorStrategy comme Brad Mace suggéré n'est pas nécessaire car le DefaultErrorStrategy produit par défaut de très bons messages d'erreur.

J'utilise ensuite le ErrorListener personnalisé dans ma fonction d'analyse:

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 fait MyParseRules, voir ici .)

Cela vous donnera les mêmes messages d'erreur que ceux qui seraient imprimés sur la console par défaut, uniquement sous la forme d'exceptions appropriées.

71
Mouagip

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

L'exception qui a forcé cette règle à revenir. Si la règle s'est terminée avec succès, il s'agit de null.

Edit: Si vous utilisez DefaultErrorStrategy, l'exception de contexte d'analyse ne sera pas propagée jusqu'au code appelant, donc vous '' Je pourrai examiner directement le champ exception. Si vous utilisez BailErrorStrategy, le ParseCancellationException jeté par celui-ci inclura un RecognitionException si vous appelez getCause().

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

Edit 2: Sur la base de votre autre réponse, il semble que vous ne souhaitiez pas réellement d'exception, mais ce que vous voulez est une manière différente de signaler le les erreurs. Dans ce cas, vous serez plus intéressé par l'interface ANTLRErrorListener . Vous souhaitez appeler parser.removeErrorListeners() pour supprimer l'écouteur par défaut qui écrit dans la console, puis appeler parser.addErrorListener(listener) pour votre propre spécial auditeur. J'utilise souvent l'écouteur 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);
    }
}

Avec cette classe disponible, vous pouvez utiliser ce qui suit pour l'utiliser.

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

Un beaucoup exemple plus compliqué d'écouteur d'erreur que j'utilise pour identifier les ambiguïtés qui rendent une grammaire non SLL est la classe SummarizingDiagnosticErrorListener dans TestPerformance .

45
Sam Harwell

Ce que j'ai trouvé jusqu'à présent est basé sur l'extension de DefaultErrorStrategy et le remplacement de ses méthodes reportXXX (bien qu'il soit tout à fait possible que je complique les choses plus 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 lève des exceptions avec des messages utiles, et la ligne et la position du problème peuvent être obtenues à partir du jeton offending , ou si ce n'est pas défini, à partir du current jeton en utilisant ((Parser) re.getRecognizer()).getCurrentToken() sur le RecognitionException.

Je suis assez satisfait de la façon dont cela fonctionne, bien que le fait d'avoir six méthodes reportX à remplacer me fait penser qu'il existe un meilleur moyen.

9
Brad Mace