web-dev-qa-db-ja.com

ANTLR4のエラー処理

パーサーが何をすべきかわからない場合のデフォルトの動作は、次のようなメッセージを端末に出力することです。

行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ではアクションが「高度」と見なされるようになったので、奇妙な方法で物事を進めているかもしれませんが、この方法から「高度でない」方法がどのようになるかについては検討していません。私たちのニーズにうまく対応しています。

61
Brad Mace

私は既存の2つの答えに少し苦労したので、最終的に解決策を共有したいと思います。

まず、 Sam Harwell のような、独自のバージョンの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);
      }
}

DefaultErrorStrategyは後者をキャッチし、独自のコードには到達しないため、ParseCancellationExceptionの代わりにRecognitionExceptionを使用することに注意してください。

DefaultErrorStrategyはデフォルトでかなり良いエラーメッセージを生成するため、 Brad Mace のようなまったく新しいErrorStrategyを作成する必要はありません。

次に、解析関数でカスタム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の機能の詳細については、 here を参照してください。)

これにより、適切な例外の形式でのみ、デフォルトでコンソールに出力されるのと同じエラーメッセージが表示されます。

71
Mouagip

DefaultErrorStrategy または BailErrorStrategy を使用すると、 _ParserRuleContext.exception_ フィールドが設定されますエラーが発生した解析ツリー内の解析ツリーノード。このフィールドのドキュメントは次のとおりです(追加のリンクをクリックしたくない人向け)。

このルールを強制的に返す例外。ルールが正常に完了した場合、これはnullです。

Edit:DefaultErrorStrategyを使用すると、解析コンテキスト例外は呼び出し元のコードにまで伝播されないため、 exceptionフィールドを直接調べることができます。 BailErrorStrategyを使用する場合、getCause()を呼び出すと、それによってスローされるParseCancellationExceptionにはRecognitionExceptionが含まれます。

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

much文法を非表示にするあいまいさを識別するために使用するエラーリスナーのより複雑な例-SLLは SummarizingDiagnosticErrorListenerのクラス[TestPerformance )です。

45
Sam Harwell

これまでに思いついたのは、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 トークン、または設定されていない場合は currentRecognitionException((Parser) re.getRecognizer()).getCurrentToken()を使用したトークン。

6つのreportXメソッドをオーバーライドすることで、より良い方法があると思うようになりますが、これがどのように機能するかにかなり満足しています。

9
Brad Mace