通过ErrorListener累积/收集错误以在分析后进行处理



Antlr4中的ErrorListener机制非常适合记录语法错误并在解析过程中对其做出决策,但在解析完成后,它可以更好地处理批处理错误。您可能希望在解析完成后处理错误的原因有很多,包括:

  • 我们需要一种干净的方法来以编程方式检查解析过程中的错误,并在事后处理它们
  • 有时一个语法错误会导致其他几个错误(例如,当没有在线恢复时),因此在向用户显示输出时,按父上下文对这些错误进行分组或嵌套会很有帮助,而且在解析完成之前,您无法知道所有错误
  • 您可能希望根据错误的数量和严重程度向用户显示不同的错误,例如,退出规则的单个错误或一行中恢复的几个错误可能只要求用户修复这些局部区域,否则,您可能会让用户编辑整个输入,您需要拥有所有错误才能做出此决定

底线是,如果我们知道错误发生的完整上下文(包括其他错误),我们可以更明智地报告和要求用户修复语法错误。为此,我有以下三个目标:

  1. 来自给定解析的所有错误的完整集合
  2. 每个错误的上下文信息,以及
  3. 每个错误的严重性和恢复信息

我已经编写了执行#1和#2的代码,我正在寻求#3的帮助。我还将建议一些小的改变,让#1和#2对每个人来说都更容易。

首先,为了完成#1(一个完整的错误集合),我创建了CollectionErrorListener,如下所示:

public class CollectionErrorListener extends BaseErrorListener {
private final List<SyntaxError> errors = new ArrayList<SyntaxError>();
public List<SyntaxError> getErrors() {
return errors;
}
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
if (e == null) {
// e is null when the parser was able to recover in line without exiting the surrounding rule.
e = new InlineRecognitionException(msg, recognizer, ((Parser)recognizer).getInputStream(), ((Parser)recognizer).getContext(), (Token) offendingSymbol);
}
this.errors.add(new SyntaxError(msg, e));
}  
}

这是我的InlineRecognitionException类:

public class InlineRecognitionException extends RecognitionException {
public InlineRecognitionException(String message, Recognizer<?, ?> recognizer, IntStream input, ParserRuleContext ctx, Token offendingToken) {
super(message, recognizer, input, ctx);
this.setOffendingToken(offendingToken);
}    
}

这是我的SyntaxError容器类:

public class SyntaxError extends RecognitionException {
public SyntaxError(String message, RecognitionException e) {
super(message, e.getRecognizer(), e.getInputStream(), (ParserRuleContext) e.getCtx());
this.setOffendingToken(e.getOffendingToken());
this.initCause(e);
}
}

这与280Z28对Antlr错误/异常处理的回答所引用的SyntaxErrorListener非常相似。由于CollectionErrorListener.SyntaxError的参数是如何填充的,我需要InlineRecognitionException和SyntaxError包装器。

首先,如果解析器从行中的异常中恢复,则RecognitionException参数"e"为null(不离开规则)。我们不能仅仅实例化一个新的RecognitionException,因为没有构造函数或方法允许我们设置有问题的令牌。无论如何,能够区分在线恢复的错误(使用instanceof test)是实现目标#3的有用信息,因此我们可以使用InlineRecognitionException类来指示在线恢复。

接下来,我们需要SyntaxError包装类,因为即使RecognitionException"e"不为null(例如,当恢复不在行中时),e.getMessage()的值也为null(出于某种未知原因)。因此,我们需要将msg参数存储到CollectionErrorListener.syntaxError中。因为RecognitionException上没有setMessage()修饰符方法,而且我们不能直接实例化一个新的RecognitionException(我们丢失了前一段中讨论的有问题的令牌信息),所以我们只能进行子类化,以便能够适当地设置消息、有问题的标记和原因。

这个机制运行得非常好:

CollectionErrorListener collector = new CollectionErrorListener();
parser.addErrorListener(collector);
ParseTree tree = parser.prog();
//  ...  Later ...
for (SyntaxError e : collector.getErrors()) {
// RecognitionExceptionUtil is my custom class discussed next.
System.out.println(RecognitionExceptionUtil.formatVerbose(e));
}

这就是我的下一点。格式化RecognitionException的输出有点烦人。《最终ANTLR 4参考书》的第9章展示了显示高质量错误消息意味着你需要拆分输入行,反转规则调用堆栈,并从有问题的令牌中拼凑出大量内容来解释错误发生的位置。而且,如果您在解析完成后报告错误,则以下命令不起作用:

// The following doesn't work if you are not reporting during the parse because the
// parser context is lost from the RecognitionException "e" recognizer.
List<String> stack = ((Parser)e.getRecognizer()).getRuleInvocationStack();

问题是我们丢失了RuleContext,这是getRuleInvocationStack所需要的。幸运的是,RecognitionException保留了我们上下文的副本,getRuleInvocationStack接受了一个参数,因此以下是我们在解析完成后获得规则调用堆栈的方法:

// Pass in the context from RecognitionException "e" to get the rule invocation stack
// after the parse is finished.
List<String> stack = ((Parser)e.getRecognizer()).getRuleInvocationStack(e.getCtx());

一般来说,如果我们在RecognitionException中有一些方便的方法,使错误报告更加友好,那将是一件特别好的事情。这是我第一次尝试一个实用的方法类,它可能是RecognitionException的一部分:

public class RecognitionExceptionUtil {
public static String formatVerbose(RecognitionException e) {
return String.format("ERROR on line %s:%s => %s%nrule stack: %s%noffending token %s => %s%n%s",
getLineNumberString(e),
getCharPositionInLineString(e),
e.getMessage(),
getRuleStackString(e),
getOffendingTokenString(e),
getOffendingTokenVerboseString(e),
getErrorLineStringUnderlined(e).replaceAll("(?m)^|$", "|"));
}
public static String getRuleStackString(RecognitionException e) {
if (e == null || e.getRecognizer() == null
|| e.getCtx() == null
|| e.getRecognizer().getRuleNames() == null) {
return "";
}
List<String> stack = ((Parser)e.getRecognizer()).getRuleInvocationStack(e.getCtx());
Collections.reverse(stack);
return stack.toString();
}
public static String getLineNumberString(RecognitionException e) {
if (e == null || e.getOffendingToken() == null) {
return "";
}
return String.format("%d", e.getOffendingToken().getLine());
}
public static String getCharPositionInLineString(RecognitionException e) {
if (e == null || e.getOffendingToken() == null) {
return "";
}
return String.format("%d", e.getOffendingToken().getCharPositionInLine());
}
public static String getOffendingTokenString(RecognitionException e) {
if (e == null || e.getOffendingToken() == null) {
return "";
}
return e.getOffendingToken().toString();
}
public static String getOffendingTokenVerboseString(RecognitionException e) {
if (e == null || e.getOffendingToken() == null) {
return "";
}
return String.format("at tokenStream[%d], inputString[%d..%d] = '%s', tokenType<%d> = %s, on line %d, character %d",
e.getOffendingToken().getTokenIndex(),
e.getOffendingToken().getStartIndex(),
e.getOffendingToken().getStopIndex(),
e.getOffendingToken().getText(),
e.getOffendingToken().getType(),
e.getRecognizer().getTokenNames()[e.getOffendingToken().getType()],
e.getOffendingToken().getLine(),
e.getOffendingToken().getCharPositionInLine());
}
public static String getErrorLineString(RecognitionException e) {
if (e == null || e.getRecognizer() == null
|| e.getRecognizer().getInputStream() == null
|| e.getOffendingToken() == null) {
return "";
}
CommonTokenStream tokens =
(CommonTokenStream)e.getRecognizer().getInputStream();
String input = tokens.getTokenSource().getInputStream().toString();
String[] lines = input.split(String.format("r?n"));
return lines[e.getOffendingToken().getLine() - 1];
}
public static String getErrorLineStringUnderlined(RecognitionException e) {
String errorLine = getErrorLineString(e);
if (errorLine.isEmpty()) {
return errorLine;
}
// replace tabs with single space so that charPositionInLine gives us the
// column to start underlining.
errorLine = errorLine.replaceAll("t", " ");
StringBuilder underLine = new StringBuilder(String.format("%" + errorLine.length() + "s", ""));
int start = e.getOffendingToken().getStartIndex();
int stop = e.getOffendingToken().getStopIndex();
if ( start>=0 && stop>=0 ) {
for (int i=0; i<=(stop-start); i++) {
underLine.setCharAt(e.getOffendingToken().getCharPositionInLine() + i, '^');
}
}
return String.format("%s%n%s", errorLine, underLine);
}
}

在我的RecognitionExceptionUtil中有很多需要之处(总是返回字符串,不检查识别器是否为Parser类型,不处理getErrorLineString中的多行,等等),但我希望你能理解。

我对ANTLR:未来版本的建议摘要

  1. 始终填充ANTLRErrorListener.syntaxError的"RecognitionException e"参数(包括OffendingToken),以便我们可以在解析后收集这些异常进行批处理。在进行此操作时,请确保e.getMessage()设置为返回msg参数中当前的值
  2. 为RecognitionException添加一个包含OffendingToken的构造函数
  3. 删除ANTLRErrorListener.syntaxError的方法签名中的其他参数,因为它们将是无关的并导致混淆
  4. 在RecognitionException中为常见的东西添加方便的方法,如getCharPositionInLine、getLineNumber、getRuleStack,以及上面定义的RecognitionExceptionUtil类中的其他东西。当然,这些方法必须检查null,还必须检查其中一些方法的识别器是否为Parser类型
  5. 当调用ANTLRErrorListener.syntaxError时,克隆识别器,这样我们在解析完成时就不会丢失上下文(我们可以更容易地调用getRuleInvocationStack)
  6. 如果克隆识别器,则不需要将上下文存储在RecognitionException中。我们可以对e.getCtx()进行两个更改:首先,将其重命名为e.getContext(),使其与Parser.getContext(
  7. 在RecognitionException中包括有关错误严重性以及解析器如何恢复的信息。这是我从一开始的第三个目标。如果能根据语法分析器处理语法错误的程度对其进行分类,那就太好了。这个错误是破坏了整个语法分析,还是只是显示为一个光点?跳过/插入了多少个令牌以及哪些令牌

因此,我正在寻求关于我的三个目标的反馈,尤其是收集有关目标#3的更多信息的任何建议:每个错误的严重性和恢复信息。

我将这些建议发布到了Antlr4 GitHub问题列表中,并收到了以下回复。我相信ANTLRErrorListener.syntaxError方法包含冗余/令人困惑的参数,需要大量API知识才能正确使用,但我理解这个决定。这是问题的链接和文本回复的副本:

发件人:https://github.com/antlr/antlr4/issues/396

关于您的建议:

  1. 将RecognitionException e参数填充到syntaxError:如文档中所述:

RecognitionException对于所有语法错误都不是null,除非我们发现不匹配的令牌错误,我们可以从内联中恢复,而不从周围的规则返回(通过单个令牌插入和删除机制)。

  1. 使用有问题的令牌向RecognitionException添加构造函数:这与此问题无关,将单独解决(如果有的话)
  2. 从syntaxError中删除参数:这不仅会给在以前版本的ANTLR 4中实现此方法的用户带来突破性的更改,而且会消除报告内联错误(即没有RecognitionException可用的错误)的可用信息的能力
  3. RecognitionException中的便利方法:这与这个问题并不真正相关,将单独解决(如果有的话)。(进一步注意:编写API文档已经够难的了。这只是增加了更多可以轻松访问的方法,所以我反对这种更改。)
  4. 调用syntaxError时克隆识别器:这是一种性能关键的方法,因此只有在绝对必要时才会创建新对象
  5. "如果克隆识别器":在调用syntaxError之前,永远不会克隆识别器
  6. 如果应用程序需要,这些信息可以存储在ANTLRErrorListener和/或ANTLRErrorStrategy实现中的关联映射中

我暂时关闭这个问题,因为我没有从这个列表中看到任何需要更改运行时的操作项。

相关内容

  • 没有找到相关文章

最新更新