乐者为王

Do one thing, and do it well.

ANTLR 4权威参考读书笔记(30)

有时我们在调试语法时需要ANTLR提供更好的消息提示,为达到这个目的,我们可以改变ANTLR的标准错误报告。

改变和重定向ANTLR错误消息

默认情况下,ANTLR把所有的错误发送给标准错误,但我们可以通过提供ANTLRErrorListener接口的实现来修改目标和内容。该接口有syntaxError()方法可以应用于词法分析器和语法分析器。方法syntaxError()接收各种有关错误的位置以及错误消息的信息。它也接收语法分析器的一个引用,因此我们可以查询关于识别的状态。

例如,这里是一个错误监听器,它打印规则调用栈以及随后的用有问题的记号信息来加强的通常错误消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static class VerboseListener extends BaseErrorListener {
    @Override
    public void syntaxError(Recognizer<?, ?> recognizer,
                            Object offendingSymbol,
                            int line, int charPositionInLine,
                            String msg,
                            RecognitionException e)
    {
        List<String> stack = ((Parser)recognizer).getRuleInvocationStack();
        Collections.reverse(stack);
        System.err.println("rule stack: "+stack);
        System.err.println("line "+line+":"+charPositionInLine+" at "+
        offendingSymbol+": "+msg);
    }
}

使用这个定义,我们的应用可以很容易地在调用开始规则前给语法分析器添加一个错误监听器。

1
2
3
4
SimpleParser parser = new SimpleParser(tokens);
parser.removeErrorListeners();    // remove ConsoleErrorListener
parser.addErrorListener(new VerboseListener());    // add ours
parser.prog();    // parse as usual

在添加我们自己的错误监听器之前,必须先移除标准的终端错误监听器,否则的话就会得到重复的错误消息。

现在让我们看下包含一个额外类名以及缺少字段名的类定义的错误消息看起来是什么样子:

1
2
compile *.java
run TestE_Listener

以下是包含额外类名以及缺少字段名的类定义:

1
2
3
class T T {
  int ;
}

然后我们就会看到如下错误消息:

1
2
3
4
5
rule stack: [prog, classDef]
line 1:8 at [@2,8:8='T',<9>,1:8]: extraneous input 'T' expecting '{'
rule stack: [prog, classDef, member]
line 2:6 at [@5,18:18=';',<8>,2:6]: no viable alternative at input 'int;'
class T

栈[prog, classDef]指出语法分析器在规则classDef中,且被prog调用。注意,记号信息包含在输入流中的字符位置,这在高亮输入中的错误时是非常有用的。例如,记号[@2,8:8='T',<9>,1:8]指出它是记号流中的第3个记号(索引从0开始),字符范围从8到8,记号类型为9,位于第1行,并且在字符位置8(计数从0开始,且TAB被当作一个字符)。

作为另一个示例,让我们构建一个错误监听器TestE_Listener2.java,它打印带有被下划线强调的有错误的记号的行。

1
2
compile *.java
run TestE_Listener2

输入以下数据:

1
2
3
class T XYZ {
  int ;
}

然后就会看如如下错误信息:

1
2
3
4
5
6
7
line 1:8 extraneous input 'XYZ' expecting '{'
class T XYZ {
        ^^^
line 2:6 no viable alternative at input 'int;'
  int ;
      ^
class T

为了让事情变得更容易,我们将忽略TAB——charPositionInLine不是列号,因为TAB的大小不是统一定义的。这里是一个错误监听器实现,强调输入中的错误位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static class UnderlineListener extends BaseErrorListener {
    public void syntaxError(Recognizer<?, ?> recognizer,
                            Object offendingSymbol,
                            int line, int charPositionInLine,
                            String msg,
                            RecognitionException e) {
        System.err.println("line "+line+":"+charPositionInLine+" "+msg);
        underlineError(recognizer,(Token)offendingSymbol,
        line, charPositionInLine);
    }

    protected void underlineError(Recognizer recognizer,
                                  Token offendingToken, int line,
                                  int charPositionInLine) {
        CommonTokenStream tokens =
            (CommonTokenStream)recognizer.getInputStream();
        String input = tokens.getTokenSource().getInputStream().toString();
        String[] lines = input.split("\n");
        String errorLine = lines[line - 1];
        System.err.println(errorLine);
        for (int i=0; i<charPositionInLine; i++) System.err.print(" ");
        int start = offendingToken.getStartIndex();
        int stop = offendingToken.getStopIndex();
        if ( start>=0 && stop>=0 ) {
            for (int i=start; i<=stop; i++) System.err.print("^");
        }
        System.err.println();
    }
}

关于错误监听器还有一件事需要知道。当语法分析器侦测到一个模棱两可的输入序列时,它会通知错误监听器。默认的错误监听器ConsoleErrorListener实际上不会在终端打印任何东西,也就是说,语法分析器不会通知用户。让我们回顾下能用两种方式匹配输入f();的那段歧义语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
grammar Ambig;

stat: expr ';'    // expression statement
    | ID '(' ')' ';'    // function call statement
    ;

expr: ID '(' ')'
    | INT
    ;

INT : [0-9]+ ;
ID  : [a-zA-Z]+ ;
WS  : [ \t\r\n]+ -> skip ;

如果我们测试这段语法,我们不会看到有关模棱两可的输入的警告。

1
2
3
antlr Ambig.g
compile *.java
grun Ambig stat

然后输入:

1
f();

Comments