Xử lý lỗi trong ANTLR4


82

Hành vi mặc định khi trình phân tích cú pháp không biết phải làm gì là in các thông báo tới thiết bị đầu cuối như:

dòng 1:23 thiếu DECIMAL tại '}'

Đây là một thông điệp tốt, nhưng không đúng chỗ. Tôi muốn nhận điều này như một ngoại lệ.

Tôi đã thử sử dụng BailErrorStrategy, nhưng điều này ném ra một ParseCancellationExceptionmà không có thông báo (gây ra bởi a InputMismatchException, cũng không có thông báo).

Có cách nào để tôi có thể báo cáo lỗi thông qua các ngoại lệ trong khi vẫn giữ lại thông tin hữu ích trong tin nhắn không?


Đây là những gì tôi thực sự theo đuổi - Tôi thường sử dụng các hành động trong quy tắc để xây dựng một đối tượng:

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

Sau đó, khi tôi gọi trình phân tích cú pháp, tôi làm như sau:

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

Tất cả những gì tôi thực sự muốn là

  • để dataspec()cuộc gọi ném ra một ngoại lệ (lý tưởng là một ngoại lệ đã được chọn) khi đầu vào không thể phân tích cú pháp
  • ngoại lệ đó để có một thông báo hữu ích và cung cấp quyền truy cập vào số dòng và vị trí nơi phát hiện ra sự cố

Sau đó, tôi sẽ để ngoại lệ đó làm bong bóng ngăn gọi đến bất kỳ nơi nào phù hợp nhất để trình bày một thông báo hữu ích cho người dùng - giống như cách tôi xử lý kết nối mạng bị ngắt, đọc tệp bị hỏng, v.v.

Tôi đã thấy rằng các hành động hiện được coi là "nâng cao" trong ANTLR4, vì vậy có thể tôi đang xem xét mọi thứ theo một cách kỳ lạ, nhưng tôi chưa xem xét cách "không nâng cao" để làm điều này là gì kể từ đó đã hoạt động tốt cho nhu cầu của chúng tôi.

Câu trả lời:


97

Vì tôi đã có một chút đấu tranh với hai câu trả lời hiện có, tôi muốn chia sẻ giải pháp mà tôi đã kết thúc.

Trước hết, tôi đã tạo phiên bản ErrorListener của riêng mình như Sam Harwell đã đề xuất:

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

Lưu ý việc sử dụng a ParseCancellationExceptionthay vì a RecognitionExceptionvì DefaultErrorStrategy sẽ bắt cái sau và nó sẽ không bao giờ đạt được mã của riêng bạn.

Việc tạo ErrorStrategy hoàn toàn mới như Brad Mace đề xuất là không cần thiết vì DefaultErrorStrategy tạo ra các thông báo lỗi khá tốt theo mặc định.

Sau đó, tôi sử dụng ErrorListener tùy chỉnh trong chức năng phân tích cú pháp của mình:

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

(Để biết thêm thông tin về những gì MyParseRuleskhông, hãy xem tại đây .)

Điều này sẽ cung cấp cho bạn các thông báo lỗi giống như sẽ được in ra bảng điều khiển theo mặc định, chỉ ở dạng ngoại lệ thích hợp.


3
Tôi đã thử điều này và tôi xác nhận rằng nó hoạt động tốt. Tôi nghĩ đây là giải pháp dễ nhất trong 3 giải pháp được đề xuất.
Kami

1
Đây là con đường đúng đắn để đi. Cách đơn giản nhất để đi. "Vấn đề" xảy ra trong lexer và bạn nên báo cáo nó ngay lúc đó nếu điều quan trọng là đầu vào phải hợp lệ trước khi cố gắng phân tích cú pháp. ++
RubberDuck

Có lý do cụ thể nào để sử dụng ThrowingErrorListenerlớp làm Singleton không?
RonyHe 19/1217

@RonyHe Không, đây chỉ là bản chuyển thể của mã Sam Harwells .
Mouagip

Giải pháp này phù hợp với tôi với một lưu ý - chúng tôi đang cố gắng phân tích cú pháp bằng SLL và sau đó quay trở lại LL, và hóa ra là làm như vậy không gây ra lỗi khi thực hiện phân tích cú pháp dự phòng. Cách giải quyết là tạo một trình phân tích cú pháp hoàn toàn mới cho lần thử thứ hai thay vì đặt lại trình phân tích cú pháp - dường như việc đặt lại trình phân tích cú pháp không đặt lại được một số trạng thái quan trọng.
Trejkaz

51

Khi bạn sử dụng DefaultErrorStrategyhoặc the BailErrorStrategy, ParserRuleContext.exceptiontrường được đặt cho bất kỳ nút cây phân tích cú pháp nào trong cây phân tích cú pháp kết quả xảy ra lỗi. Tài liệu cho trường này đọc (cho những người không muốn nhấp vào một liên kết bổ sung):

Ngoại lệ buộc quy tắc này phải trả lại. Nếu quy tắc được hoàn thành thành công, đây là null.

Chỉnh sửa: Nếu bạn sử dụng DefaultErrorStrategy, ngoại lệ ngữ cảnh phân tích cú pháp sẽ không được truyền đến mã gọi, vì vậy bạn sẽ có thể kiểm tra exceptiontrường trực tiếp. Nếu bạn sử dụng BailErrorStrategy, ParseCancellationExceptionnó sẽ bao gồm một RecognitionExceptionnếu bạn gọi getCause().

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

Chỉnh sửa 2: Dựa trên câu trả lời khác của bạn, có vẻ như bạn không thực sự muốn có ngoại lệ, nhưng điều bạn muốn là một cách khác để báo cáo lỗi. Trong trường hợp đó, bạn sẽ quan tâm hơn đến ANTLRErrorListenergiao diện. Bạn muốn gọi parser.removeErrorListeners()để xóa trình nghe mặc định ghi vào bảng điều khiển, sau đó gọi parser.addErrorListener(listener)trình nghe đặc biệt của riêng bạn. Tôi thường sử dụng trình nghe sau làm điểm bắt đầu, vì nó bao gồm tên của tệp nguồn với các thông báo.

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

Với lớp này có sẵn, bạn có thể sử dụng như sau để sử dụng nó.

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

Một nhiều ví dụ phức tạp hơn của một người biết lắng nghe lỗi mà tôi sử dụng để xác định sự mơ hồ mà làm cho một văn phạm phi SLL là SummarizingDiagnosticErrorListenerlớp trongTestPerformance .


Ok ... làm cách nào để sử dụng nó? Tôi có phải sử dụng một cái gì đó như ((InputMismatchException) pce.getCause()).getCtx().exceptionđể nhận được thông báo lỗi hữu ích không?
Brad Mace

1
Tôi đã thử nghiệm một chút với việc ném ngoại lệ từ trình nghe lỗi, nhưng ngoại lệ dường như không bao giờ hiển thị. Tôi vừa kết thúc với NPE từ các hành động trong ngữ pháp do không trùng khớp. Tôi đã thêm một số cốt truyện vào câu hỏi vì có vẻ như tôi đang bơi ngược dòng điện.
Brad Mace

Bạn chỉ nên viết một lớp tiện ích để trả về "dòng", "cột" và "thông báo" từ a RecognitionException. Thông tin bạn muốn có sẵn trong ngoại lệ đã được ném.
Sam Harwell

Quý độc giả ơi, nếu bạn giống tôi, bạn đang tự hỏi REPORT_SYNTAX_ERRORS là gì. Dưới đây là câu trả lời: stackoverflow.com/questions/18581880/handling-errors-in-antlr-4
james.garriss

Ví dụ này thực sự hữu ích. Tôi nghĩ rằng nó nên ở đâu đó trong tài liệu chính thức , nó dường như thiếu một trang để xử lý lỗi. Ít nhất đề cập đến người nghe lỗi sẽ là tốt.
geekley

10

Những gì tôi đưa ra cho đến nay dựa trên việc mở rộng DefaultErrorStrategyvà ghi đè reportXXXcác phương thức của nó (mặc dù hoàn toàn có thể tôi đang làm mọi thứ phức tạp hơn mức cần thiết):

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

Điều này ném ra các ngoại lệ với các thông báo hữu ích và dòng và vị trí của vấn đề có thể được lấy từ offendingmã thông báo hoặc nếu điều đó chưa được đặt, từ currentmã thông báo bằng cách sử dụng ((Parser) re.getRecognizer()).getCurrentToken()trên RecognitionException.

Tôi khá hài lòng với cách điều này đang hoạt động, mặc dù có sáu reportXphương pháp để ghi đè khiến tôi nghĩ rằng có một cách tốt hơn.


hoạt động tốt hơn cho c #, chấp nhận và trên bình chọn câu trả lời có lỗi biên soạn trong c #, một số không tương thích của Generics luận IToken vs int
SARH

0

Đối với bất kỳ ai quan tâm, đây là ANTLR4 C # tương đương với câu trả lời của 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);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.