处理ANTLR4中的错误


80

解析器不知道要做什么时的默认行为是将消息打印到终端,例如:

第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中的动作现在被认为是“高级”的,所以也许我正在以一种奇怪的方式来处理事情,但是自从这种方式以来,我没有研究过什么是“非高级”的方式。一直很好地满足我们的需求。

Answers:


94

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

首先,我像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);
      }
}

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

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,请参见此处。)

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


3
我尝试了一下,并确认它运作良好。我认为这是3种建议的解决方案中最简单的一种。
卡米2014年

1
这是正确的方法。最简单的方法。“问题”发生在词法分析器中,因此有必要立即进行报告,如果在尝试解析之前输入有效很重要,那么就应该报告该问题。++
RubberDuck

ThrowingErrorListener类用作Singleton是否有特定原因?
RonyHe '17

@RonyHe不,这只是Sam Harwells代码的改编。
Mouagip

这个解决方案对我有用,但有一个警告-我们正在尝试使用SLL进行解析,然后又回落到LL,事实证明,这样做不会导致在进行后备解析时出现错误。解决方法是为第二次尝试构造一个全新的解析器,而不是重置解析器-显然,重置解析器无法重置某些重要状态。
Trejkaz

50

当使用DefaultErrorStrategy或时BailErrorStrategyParserRuleContext.exception将为结果分析树中发生错误的任何分析树节点设置该字段。该字段的文档为(对于不想单击额外链接的人):

迫使该规则返回的异常。如果规则成功完成,则为null

编辑:如果使用DefaultErrorStrategy,则解析上下文异常不会一直传播到调用代码,因此您将能够exception直接检查该字段。如果使用BailErrorStrategyParseCancellationException抛出的结果将包含一个RecognitionExceptionif调用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方法获取有用的错误消息?
2013年

1
我做了一些尝试,从错误侦听器抛出异常,但是异常似乎从未出现。由于匹配失败,我刚从语法中的动作中获得了NPE。我为这个问题添加了一些背景知识,因为看来我可能正在与潮流相对立。
布莱德·梅斯

您应该只编写实用程序类以从中返回“行”,“列”和“消息” RecognitionException。除了已经抛出的异常之外,您还可以使用所需的信息。
山姆·哈威尔

温柔的读者,如果您像我一样,想知道REPORT_SYNTAX_ERRORS的全部含义。答案是:stackoverflow.com/questions/18581880/handling-errors-in-antlr-4
james.garriss 2014年

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方法可以使我认为有更好的方法。


C#更好,接受和投票最多的答案在C#中有编译错误,泛型参数IToken与int不兼容
sarh,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.