Java 通过ErrorListener累积/收集错误以在解析后处理

Java 通过ErrorListener累积/收集错误以在解析后处理,java,error-handling,antlr4,Java,Error Handling,Antlr4,Antlr4中的ErrorListener机制非常适合记录语法错误并在语法错误发生时做出决策,但在语法分析完成后,它可以更好地处理批处理错误。您可能希望在解析完成后处理错误的原因有很多,包括: 我们需要一种干净的方式,在解析过程中以编程方式检查错误,并在事后处理错误 有时,一个语法错误会导致多个其他错误(例如,当未在线恢复时),因此在向用户显示输出时,按父上下文对这些错误进行分组或嵌套会很有帮助,并且在解析完成之前,您无法知道所有错误 根据错误的数量和严重程度,您可能希望以不同的方式向用户显示

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

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

  • 给定解析中所有错误的完整集合
  • 每个错误的上下文信息,以及
  • 每个错误的严重性和恢复信息
  • 我已经编写了执行#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));
        }  
    }
    
    这与280Z28的回答中提到的SyntaxErrorListener非常相似。我需要InlineRecognitionException和SyntaxError包装,因为CollectionErrorListener.SyntaxError的参数是如何填充的

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

    接下来,我们需要SyntaxError包装类,因为即使识别异常“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));
        }
    
    这就是我的下一点。设置识别异常的输出格式有点烦人。本书的第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();
    
    // 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());
    
    //如果在分析过程中没有报告,则以下操作无效,因为
    //识别异常“e”识别器中的分析器上下文丢失。
    列表堆栈=((解析器)e.getRecognizer()).getRuleInvocationStack();
    
    问题是我们丢失了RuleContext,这是getRuleInvocationStack所需要的。幸运的是,RecognitionException保留了我们上下文的一个副本,getRuleInvocationStack接受一个参数,因此在解析完成后,我们如何获得规则调用堆栈:

    // 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();
    
    // 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“e”传入上下文以获取规则调用堆栈
    //解析完成后。
    列表堆栈=((解析器)e.GetRecognitor()).getRuleInvocationStack(e.getCtx());
    
    一般来说,如果我们在RecognitionException中有一些方便的方法,以使错误报告更友好,那将特别好。这是我第一次尝试一个实用类方法,它可能是RecognitionException的一部分:

    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);
        }    
    }
    
    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);
        }
    }
    
    公共类识别例外直到{
    公共静态字符串格式详细(识别异常e){
    返回String.format(“第%s行出现错误:%s=>%s%n规则堆栈:%s%n放弃标记%s=>%s%n%s”,
    getLineNumberString(e),
    getCharPositionInLineString(e),
    e、 getMessage(),
    getRuleStackString(e),
    正在安装(e),
    GetAskingTokenVerboseString(e),
    getErrorLineStringUnderlined(e).replaceAll(“(?m)^ |$”,“|”);
    }
    公共静态字符串getRuleStackString(识别异常e){
    如果(e==null | | e.getRecognitizer()==null
    ||e.getCtx()==null
    ||e.GetRecognitor().getRuleNames()==null){
    返回“”;
    }
    列表堆栈=((解析器)e.GetRecognitor()).getRuleInvocationStack(e.getCtx());
    集合。反向(堆栈);
    返回stack.toString();
    }
    公共静态字符串getLineNumberString(识别异常e){
    如果(e==null | | e.GetOfficingToken()==null){
    返回“”;
    }
    返回String.format(“%d”,例如getAskingToken().getLine());
    }
    公共静态字符串getCharPositionInLineString(识别异常e){
    如果(e==null | | e.GetOfficingToken()==null){
    返回“”;