【问题标题】:Handling errors in ANTLR4处理 ANTLR4 中的错误
【发布时间】:2013-08-10 12:33:26
【问题描述】:

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

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

【问题讨论】:

    标签: java error-handling 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);
          }
    }
    

    请注意使用 ParseCancellationException 而不是 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 功能的更多信息,请参阅here。)

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

    【讨论】:

    • 我试过了,我确认它运行良好。我认为这是提出的 3 个解决方案中最简单的一个。
    • 这是正确的方法。最简单的方法。 “问题”发生在词法分析器中,如果在尝试解析之前输入有效很重要,那么立即报告它是有意义的。 ++
    • ThrowingErrorListener 类用作Singleton 有什么特别的原因吗?
    • @RonyHe 不,这只是Sam Harwells code的改编。
    • 这个解决方案对我有用,但有一个警告 - 我们尝试使用 SLL 进行解析,然后回退到 LL,事实证明这样做不会导致在进行回退解析时出现错误.解决方法是为第二次尝试构建一个全新的解析器,而不是重置解析器 - 显然重置解析器无法重置某些重要状态。
    【解决方案2】:

    当您使用DefaultErrorStrategyBailErrorStrategy 时,会为生成的分析树中发生错误的任何分析树节点设置ParserRuleContext.exception 字段。该字段的文档为(对于不想点击额外链接的人):

    强制此规则返回的异常。如果规则成功完成,这是null

    编辑:如果您使用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);
    

    一个非常更复杂的错误侦听器示例是SummarizingDiagnosticErrorListener class in TestPerformance

    【讨论】:

    • 好吧……我该如何利用它呢?我应该使用((InputMismatchException) pce.getCause()).getCtx().exception 之类的东西来获取有用的错误消息吗?
    • 我尝试了一些从错误侦听器中抛出异常的方法,但该异常似乎从未出现过。由于匹配失败,我刚刚从语法中的操作中得到了 NPE。我为这个问题添加了一些背景故事,因为看起来我可能会逆流而上。
    • 您应该只编写一个实用程序类来从RecognitionException 返回“行”、“列”和“消息”。您想要的信息在已经抛出的异常中可用。
    • 温柔的读者,如果你和我一样,你会想知道 REPORT_SYNTAX_ERRORS 到底是什么。这是答案:stackoverflow.com/questions/18581880/handling-errors-in-antlr-4
    • 这个例子真的很有用。我认为它应该在official documentation 的某个地方,它似乎缺少用于错误处理的页面。至少提及错误侦听器会很好。
    【解决方案3】:

    到目前为止,我的想法是基于扩展 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 令牌中获取,方法是在@987654329 上使用((Parser) re.getRecognizer()).getCurrentToken() @。

    我对它的工作方式相当满意,尽管有六个 reportX 方法可以覆盖让我觉得有更好的方法。

    【讨论】:

    • 对 c# 效果更好,接受和投票最多的答案在 c# 中有编译错误,泛型参数 IToken 与 int 的一些不兼容
    【解决方案4】:

    对于任何感兴趣的人,这里是与 Sam Harwell 的答案等效的 ANTLR4 C#:

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

    【讨论】:

      【解决方案5】:

      对于使用 Python 的人,这里是基于 Mouagip's answer 的 Python 3 中的解决方案。

      首先,定义一个自定义错误监听器:

      from antlr4.error.ErrorListener import ErrorListener
      from antlr4.error.Errors import ParseCancellationException
      
      class ThrowingErrorListener(ErrorListener):
          def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
              ex = ParseCancellationException(f'line {line}: {column} {msg}')
              ex.line = line
              ex.column = column
              raise ex
      

      然后将其设置为词法分析器和解析器:

      lexer = MyScriptLexer(script)
      lexer.removeErrorListeners()
      lexer.addErrorListener(ThrowingErrorListener())
      
      token_stream = CommonTokenStream(lexer)
      
      parser = MyScriptParser(token_stream)
      parser.removeErrorListeners()
      parser.addErrorListener(ThrowingErrorListener())
      
      tree = parser.script()
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-07-21
        • 2017-04-05
        • 2016-08-18
        • 2013-08-09
        • 2016-08-24
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多