处理 ANTLR4 中的错误

2022-08-31 11:22:55

当解析器不知道该怎么做时,默认行为是将消息打印到终端,如下所示:

第 1 行:23 在“}”处缺少小数

这是一个好信息,但在错误的地方。我宁愿把这个作为例外。

我尝试过使用 ,但这会抛出一个没有消息(由 一个 引起,也没有消息)。BailErrorStrategyParseCancellationExceptionInputMismatchException

有没有办法让它通过异常报告错误,同时保留消息中的有用信息?


以下是我真正想要的 - 我通常使用规则中的操作来构建对象:

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中被认为是“高级的”,所以也许我正在以一种奇怪的方式做事,但我还没有研究过“非高级”的方式会是什么,因为这种方式已经很好地满足了我们的需求。


答案 1

由于我对两个现有的答案有一点挣扎,我想分享我最终得到的解决方案。

首先,我创建了自己的 ErrorListener 版本,就像 Sam Harwell 建议的那样:

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

请注意,使用 a 而不是 a,因为 DefaultErrorStrategy 会捕获后者,并且永远不会到达你自己的代码。ParseCancellationExceptionRecognitionException

Brad Mace建议的那样创建一个全新的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

这将为您提供与默认情况下打印到控制台相同的错误消息,只是以适当的异常形式。


答案 2

使用 DefaultErrorStrategyBailErrorStrategy 时,将为生成的解析树中发生错误的任何解析树节点设置 ParserRuleContext.exception 字段。此字段的文档如下(对于不想单击额外链接的用户):

迫使此规则返回的例外。如果规则已成功完成,则为 。null

编辑:如果使用 ,则解析上下文异常不会一直传播到调用代码,因此您将能够直接检查该字段。如果你使用 ,它所抛出的将包括一个 if 你调用 .DefaultErrorStrategyexceptionBailErrorStrategyParseCancellationExceptionRecognitionExceptiongetCause()

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

编辑 2:根据您的其他答案,您似乎实际上并不想要异常,但您想要的是一种不同的报告错误的方式。在这种情况下,你会对ANTLRErrorListener接口更感兴趣。您希望调用 parser.removeErrorListeners() 来删除写入控制台的默认侦听器,然后为您自己的特殊侦听器调用 parser.addErrorListener(侦听器)。我经常使用以下侦听器作为起点,因为它包括消息的源文件的名称。

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 的歧义是 TestPerformance 中的 SummarizingDiagnosticErrorListener


推荐