【问题标题】:Accumulating / Collecting Errors via ErrorListener to handle after the Parse通过 ErrorListener 累积/收集错误以在 Parse 之后处理
【发布时间】:2013-12-29 18:58:00
【问题描述】:

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 error/exception handling 的回答中提到的 SyntaxErrorListener 非常相似。我需要 InlineRecognitionException 和 SyntaxError 包装器,因为 CollectionErrorListener.syntaxError 的参数是如何填充的。

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

接下来,我们需要 SyntaxError 包装类,因为即使 RecognitionException "e" 不为空(例如,当恢复不在行中时),e.getMessage() 的值也为空(出于某种未知原因)。因此,我们需要将 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 格式化输出有点烦人。 The Definitive ANTLR 4 Reference 书的第 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() 一致,其次,使其成为我们在 RecognitionException (检查识别器是 Parser 的一个实例)。
  7. 在 RecognitionException 中包含有关错误严重性和解析器如何恢复的信息。这是我从一开始的目标#3。根据解析器处理语法错误的程度对语法错误进行分类会很棒。这个错误是炸毁了整个解析还是只是显示为一个亮点?跳过/插入了多少和哪些令牌?

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

【问题讨论】:

    标签: java error-handling antlr4


    【解决方案1】:

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

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

    关于您的建议:

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

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

    1. 使用违规标记向 RecognitionException 添加构造函数:这与此问题无关,将单独解决(如果有的话)。
    2. Removing parameters from syntaxError:这不仅会为在以前版本的 ANTLR 4 中实现此方法的用户引入重大更改,而且会消除报告内联发生的错误的可用信息的能力(即错误RecognitionException 可用)。
    3. RecognitionException 中的便捷方法:这与此问题并不真正相关,将单独解决(如果有的话)。 (进一步说明:按原样记录 API 已经够难了。这只是增加了更多方法来做已经很容易访问的事情,所以我反对这种改变。)
    4. 调用 syntaxError 时克隆识别器:这是一种性能关键方法,因此仅在绝对必要时才会创建新对象。
    5. “如果克隆识别器”:识别器在调用syntaxError之前永远不会被克隆。
    6. 如果您的应用程序需要,此信息可以存储在您的 ANTLRErrorListener 和/或 ANTLRErrorStrategy 实现中的关联映射中。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-06-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多