繁体   English   中英

处理 ANTLR4 中的错误

[英]Handling errors in ANTLR4

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

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

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

首先,我创建了自己的 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功能的更多信息,请参见此处。)

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

当您使用DefaultErrorStrategyBailErrorStrategyParserRuleContext.exception字段会为结果解析树中发生错误的任何解析树节点设置。 该字段的文档如下(对于不想单击额外链接的人):

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

编辑:如果您使用DefaultErrorStrategy ,解析上下文异常将不会一直传播到调用代码,因此您将能够直接检查exception字段。 如果您使用BailErrorStrategy ,它抛出的ParseCancellationException将包括一个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是含糊不清的一个复杂的例子SummarizingDiagnosticErrorListenerTestPerformance

到目前为止,我提出的是基于扩展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方法让我认为有更好的方法。

对于任何感兴趣的人,这里是 ANTLR4 C# 相当于 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);

对于使用 Python 的人,这里是基于Mouagip 的答案的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()

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM