ANTLR4でのエラーの処理


83

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

1:23行目 '}'にDECIMALがありません

これは良いメッセージですが、間違った場所にあります。例外としてこれを受け取りたいです。

を使用してみましBailErrorStrategyParseCancellationExceptionが、メッセージなしでがスローされます(、によって引き起こされ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ではアクションが「高度」と見なされるようになったので、奇妙な方法で物事を進めているのかもしれませんが、この方法以降、これを行う「高度でない」方法がどうなるかについては調べていません。私たちのニーズのためにうまく機能しています。

回答:


98

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

以下のようにまず第一に、私はは、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ください。

以下のような全く新しいErrorStrategy作成ブラッドメイスがDefaultErrorStrategyは、デフォルトではかなり良いエラーメッセージを生成するので提案する必要はありません。

次に、解析関数でカスタム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は、こちらを参照してください。)

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


3
私はこれを試しましたが、うまく機能したことを確認しました。これは、提案された3つのソリューションの中で最も簡単だと思います。
カミ

1
これが正しい方法です。最も簡単な方法です。「問題」はレクサーで発生し、解析を試みる前に入力が有効であることが重要な場合は、その場で報告するのが理にかなっています。++
RubberDuck 2015

ThrowingErrorListenerクラスをシングルトンとして使用する特別な理由はありますか?
RonyHe 2017

@RonyHeいいえ、これはSamHarwellsコードの単なる適応です
Mouagip 2017

このソリューションは、1つの注意点がありました。SLLを使用して解析してからLLにフォールバックしようとしていますが、フォールバック解析を実行してもエラーが発生しなかったことがわかりました。回避策は、パーサーをリセットする代わりに、2回目の試行でまったく新しいパーサーを構築することでした。明らかに、パーサーをリセットしても、いくつかの重要な状態をリセットできません。
Trejkaz

51

あなたが使用している場合DefaultErrorStrategyBailErrorStrategyParserRuleContext.exceptionフィールドは、エラーが発生したパースツリー内の任意の解析ツリーノードに設定されています。このフィールドのドキュメントには、次のようなものがあります(追加のリンクをクリックしたくない人向け)。

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

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

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

文法を非SLLにするあいまいさを識別するために使用するエラーリスナーのはるかに複雑な例は、のSummarizingDiagnosticErrorListenerクラスですTestPerformance


わかりました...どうすればそれを利用できますか?((InputMismatchException) pce.getCause()).getCtx().exception有用なエラーメッセージを取得するためにのようなものを使用することになっていますか?
ブラッドメイス

1
エラーリスナーから例外をスローして少し実験しましたが、例外が表示されないようです。一致しなかったため、文法のアクションからNPEが発生しました。私は流れに逆らって泳いでいるように見えるので、質問にいくつかの裏話を追加しました。
ブラッドメイス

から「行」、「列」、「メッセージ」を返すユーティリティクラスを作成するだけRecognitionExceptionです。必要な情報は、すでにスローされている例外を除いて利用できます。
サムハーウェル2013

優しい読者、あなたが私のようなら、あなたはREPORT_SYNTAX_ERRORSがすべてについて何であるか疑問に思っています。答えは次のとおりです:stackoverflow.com/questions/18581880/handling-errors-in-antlr-4
james.garriss 2014年

この例は本当に便利です。公式ドキュメントのどこかにあるはずだと思いますが、エラー処理のページがないようです。少なくともエラーリスナーについて言及するのは良いことです。
geekley

10

私がこれまでに思いついたのは、そのメソッドを拡張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ように設定されていない場合から、トークン、またはcurrent使用することによってトークン((Parser) re.getRecognizer()).getCurrentToken()RecognitionException

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


c#でより適切に機能し、承認され、投票数の多い回答でc#にコンパイルエラーが発生し、ジェネリック引数ITokenとintの非互換性がありました
sarhDec

0

興味のある方のために、SamHarwellの回答に相当するANTLR4C#を次に示します。

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);
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.