《ANTLR 4权威指南》第四章:一个快速导览

965 阅读21分钟

到目前为止,我们已经学习了如何安装ANTLR,并了解了构建语言应用程序所需的关键过程、术语和构建模块。在本章中,我们将通过一系列示例的快速浏览来了解ANTLR。出于简洁起见,我们将忽略许多细节,所以如果事情不是非常清晰,也不必担心。我们的目标只是让你对ANTLR的功能有所了解。从第5章“设计语法”(第57页)开始,我们将深入探讨其内部原理。对于那些有使用过以前版本ANTLR经验的人来说,本章是重新熟悉的绝佳方式。

本章分为四个广泛的主题,很好地展示了ANTLR的功能集。建议下载本书的代码(或按照电子书版本中的链接)并随着示例逐步进行。这样,您就会熟悉处理语法文件和构建ANTLR应用程序的过程。请记住,文本中穿插的许多代码片段并非完整文件,这样我们就可以专注于有趣的部分。

首先,我们将使用一个简单算术表达式语言的语法。我们将首先使用ANTLR的内置测试工具进行测试,然后学习在第3.3节“将生成的解析器集成到Java程序中”(第26页)中展示的启动解析器的样板主程序。接下来,我们将查看表达式语法的非平凡的解析树。(回顾一下,解析树记录了解析器如何匹配输入短语。)为了处理非常大的语法,我们将看到如何使用语法导入将语法拆分成可管理的块。接下来,我们将检查ANTLR生成的解析器对无效输入的响应情况。

其次,在查看算术表达式解析器之后,我们将使用访问者模式构建一个遍历表达式语法解析树的计算器。ANTLR解析器会自动生成访问者接口和空白方法实现,以便我们可以轻松入手。

第三,我们将构建一个翻译器,它可以读取Java类定义并生成一个基于该类中方法的Java接口。我们的实现将使用ANTLR自动生成的树监听器机制。

第四,我们将学习如何将操作(任意代码)直接嵌入语法中。大多数情况下,我们可以使用访问者或监听器构建语言应用程序,但为了获得最大的灵活性,ANTLR允许我们将自己的应用程序特定代码注入到生成的解析器中。这些操作在解析过程中执行,可以收集信息或生成输出,就像任何其他任意代码片段一样。结合语义谓词(布尔表达式),我们甚至可以在运行时使语法的某些部分消失!例如,我们可能希望在Java语法中打开和关闭枚举关键字,以解析语言的不同版本。如果没有语义谓词,我们需要两个不同版本的语法。

最后,我们将详细介绍ANTLR在词法(标记)级别的几个特性。我们将看到ANTLR如何处理包含多种语言的输入文件。然后,我们将研究令人惊叹的TokenStreamRewriter类,它使我们能够在不影响原始输入流的情况下调整、篡改或以其他方式操作标记流。最后,我们将重新访问我们的接口生成器示例,了解ANTLR如何在Java解析期间忽略空格和注释,但保留它们以供后续处理。

让我们开始我们的导览,熟悉ANTLR语法符号。请确保您已经定义了antlr4和grun的别名或脚本,如第1.2节“执行ANTLR和测试识别器”(第6页)所述。

匹配算术表达式语言

作为我们的第一个语法,我们将构建一个简单的计算器。选择处理表达式是因为它们非常常见。为了保持简单,我们只允许基本的算术运算符(加法、减法、乘法和除法)、带括号的表达式、整数数字和变量。我们还将限制为使用整数而不是允许浮点数。

下面是一些示例输入,展示了语言的所有特性:

193 
a=5 
b=6 
a+b*2 
(1+2)*3

在我们的表达式语言中,程序是由换行符终止的语句序列。一个语句可以是一个表达式、一个赋值语句,或者是空行。以下是一个ANTLR语法,可以解析这些语句和表达式:

grammar Expr;

/** The start rule; begin parsing here. */
prog:   stat+ ; 

stat:   expr NEWLINE                
    |   ID '=' expr NEWLINE        
    |   NEWLINE                   
    ;

expr:   expr ('*'|'/') expr   
    |   expr ('+'|'-') expr   
    |   INT                    
    |   ID                    
    |   '(' expr ')'         
    ;

ID  :   [a-zA-Z]+ ;      // match identifiers <label id="code.tour.expr.3"/>
INT :   [0-9]+ ;         // match integers
NEWLINE:'\r'? '\n' ;     // return newlines to parser (is end-statement signal)
WS  :   [ \t]+ -> skip ; // toss out whitespace

在不过多详述的情况下,让我们来看一下ANTLR语法符号的一些关键要素:

  • 语法由一组规则组成,描述了语言的语法结构。有关语法结构的规则(如stat和expr)以及标识符和整数等词汇符号(标记)的规则。
  • 以小写字母开头的规则组成解析器规则。
  • 以大写字母开头的规则组成词法(标记)规则。
  • 我们使用 | 运算符将规则的不同选择分隔开,还可以使用括号将符号分组为子规则。例如,子规则 ('*'|'/') 匹配乘法符号或除法符号。

当我们进入第5章“设计语法”(第57页)时,我们将详细讨论所有这些内容。

ANTLR v4最重要的新功能之一是它能够处理(大多数类型的)左递归规则。左递归规则是指在一个选择的开头调用自身的规则。例如,在这个语法中,规则expr在第11行和第12行有递归调用expr的选择。用这种方式指定算术表达式的表示法比典型的自顶向下解析器策略要简单得多。在那种策略中,我们需要多个规则,每个操作符优先级级别一个规则。关于这个特性的更多信息,请参见第5.4节“处理优先级、左递归和结合性”(第69页)。

对于那些有正则表达式经验的人来说,标记定义的符号表示法应该是熟悉的。在第6章“探索一些真实的语法”(第83页)中,我们将看到很多词法(标记)规则。唯一不寻常的语法是WS空格规则上的-> skip操作。它是一个指令,告诉词法分析器匹配但丢弃空格。(每个可能的输入字符必须由至少一个词法规则匹配。)我们通过使用正式的ANTLR符号表示法而不是在语法中使用任意代码片段告诉词法分析器跳过空格,避免将语法与特定的目标语言绑定。

好的,让我们来测试Expr语法。如果您正在查看电子书版本,可以通过单击上一个代码清单中的tour/Expr.g4链接来下载它,或者将语法复制粘贴到名为Expr.g4的文件中。

测试语法的最简单方法是使用内置的TestRig,我们可以使用别名grun来访问它。例如,在Unix系统上,以下是构建和测试的顺序:

$ antlr4 Expr.g4
$ ls Expr*.java
ExprBaseListener.java ExprListener.java
ExprLexer.java ExprParser.java

$ javac Expr*.java
$ grun Expr prog -gui t.expr # launches org.antlr.v4.runtime.misc.TestRig

由于使用了-gui选项,测试工具弹出一个窗口显示解析树,如图2所示,解析树窗口,第35页。

解析树类似于我们的解析器在识别输入时跟踪的函数调用树。(ANTLR为每个规则生成一个函数。)

截屏2023-07-10 16.50.49.png

使用测试工具来开发和测试语法是可以的,但最终我们需要将ANTLR生成的解析器集成到应用程序中。下面的主程序显示了创建所有必要对象并从规则prog开始启动我们的表达式语言解析器所需的代码:

/***
 * Excerpted from "The Definitive ANTLR 4 Reference",
 * published by The Pragmatic Bookshelf.
 * Copyrights apply to this code. It may not be used to create training material, 
 * courses, books, articles, and the like. Contact us if you are in doubt.
 * We make no guarantees that this code is fit for any purpose. 
 * Visit http://www.pragmaticprogrammer.com/titles/tpantlr2 for more book information.
***/
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;
import java.io.FileInputStream;
import java.io.InputStream;
public class ExprJoyRide {
    public static void main(String[] args) throws Exception {
        String inputFile = null; 
        if ( args.length>0 ) inputFile = args[0];
        InputStream is = System.in;
        if ( inputFile!=null ) is = new FileInputStream(inputFile);
        ANTLRInputStream input = new ANTLRInputStream(is); 
        ExprLexer lexer = new ExprLexer(input); 
        CommonTokenStream tokens = new CommonTokenStream(lexer); 
        ExprParser parser = new ExprParser(tokens); 
        ParseTree tree = parser.prog(); // parse; start at prog <label id="code.tour.main.6"/>
        System.out.println(tree.toStringTree(parser)); // print tree as text <label id="code.tour.main.7"/>
    }
}

第7到11行创建了用于词法分析器的字符输入流。第12到14行创建了词法分析器和语法分析器对象,并在它们之间创建了一个令牌流“管道”。第15行实际上启动了语法分析器。(调用规则方法就像调用该规则;我们可以调用任何我们想要的语法规则方法。)最后,第16行以文本形式打印出从规则方法prog()返回的解析树。

以下是如何构建测试程序并在输入文件t.expr上运行它的方法:

➾ $ javac ExprJoyRide.java Expr*.java
➾ $ java ExprJoyRide t.expr

❮ (prog
(stat (expr 193) \n)
(stat a = (expr 5) \n)
(stat b = (expr 6) \n)
(stat (expr (expr a) + (expr (expr b) * (expr 2))) \n)
(stat (expr (expr ( (expr (expr 1) + (expr 2)) )) * (expr 3)) \n)
)

解析树的(稍微清理过的)文本表示形式不像可视化表示形式那样易读,但对功能测试很有用。

这个表达式语法相当简单,但是语法可能会有数千行。在下一节中,我们将学习如何使这样的大型语法更易管理。

导入语法

将非常大的语法分解为逻辑块是一个好主意,就像我们在软件中所做的一样。一种方法是将语法分割为解析器和词法分析器的语法。这不是一个坏主意,因为不同的语言在词法上有惊人的重叠。例如,标识符和数字在大多数语言中通常是相同的。将词法规则提取到一个“模块”中意味着我们可以将其用于不同的解析器语法。这是一个包含所有词法规则的词法分析器语法:

lexer grammar CommonLexerRules; // note "lexer grammar"

ID  :   [a-zA-Z]+ ;      // match identifiers
INT :   [0-9]+ ;         // match integers
NEWLINE:'\r'? '\n' ;     // return newlines to parser (end-statement signal)
WS  :   [ \t]+ -> skip ; // toss out whitespace

现在我们可以用导入语句替换原始语法中的词法规则。

grammar LibExpr;         // Rename to distinguish from original
import CommonLexerRules; // includes all rules from CommonLexerRules.g4
/** The start rule; begin parsing here. */
prog:   stat+ ; 

stat:   expr NEWLINE                
    |   ID '=' expr NEWLINE        
    |   NEWLINE                   
    ;

expr:   expr ('*'|'/') expr   
    |   expr ('+'|'-') expr  
    |   INT                    
    |   ID                    
    |   '(' expr ')'    
    ;

构建和测试步骤与没有导入时相同。我们不对导入的语法本身运行ANTLR。

➾ $ antlr4 LibExpr.g4 # automatically pulls in CommonLexerRules.g4
➾ $ ls Lib*.java
❮ LibExprBaseListener.java LibExprLexer.java

➾ $ javac LibExpr*.java
➾ $ grun LibExpr prog -tree
➾ 3+4 
➾ EOF

LibExprListener.java
LibExprParser.java
❮ (prog (stat (expr (expr 3) + (expr 4)) \n))

到目前为止,我们假设输入是有效的,但错误处理是几乎所有语言应用程序的重要部分。让我们看看ANTLR如何处理错误输入。

处理错误输入

ANTLR解析器会自动报告并从语法错误中恢复。例如,如果我们在表达式中忘记了闭合括号,解析器会自动发出错误消息。

➾ $ java ExprJoyRide
➾ (1+2 
➾ 3 
➾ EOF

❮ line 1:4 mismatched input '\n' expecting {')', '+', '*', '-', '/'} (prog
     (stat (expr ( (expr (expr 1) + (expr 2)) <missing ')'>) \n)
     (stat (expr 3) \n)
   )

同样重要的是,解析器能够正确匹配第二个表达式(3),从而进行恢复。

在使用grun的-gui选项时,解析树对话框会自动以红色突出显示错误节点。

$ grun LibExpr prog -gui 
➾ (1+234*69EOF

截屏2023-07-10 17.02.15.png

请注意,ANTLR成功地从第一个表达式的错误中恢复,以正确匹配第二个表达式。

ANTLR的错误处理机制具有很大的灵活性。我们可以更改错误消息、捕获识别异常,甚至修改基本的错误处理策略。我们将在第9章《错误报告和恢复》中进行讨论。

这就完成了我们对语法和解析的快速介绍。我们已经看过了一个简单的表达式语法,以及如何使用内置的测试工具和示例主程序来启动它。我们还了解了如何获取解析树的文本和可视化表示,以显示我们的语法如何识别输入短语。导入语句允许我们将语法分割成模块。现在,让我们超越语言识别,转向解释表达式(计算其值)。

使用访问者模式构建计算器

为了使前面的算术表达式解析器能够计算值,我们需要编写一些Java代码。ANTLR v4鼓励我们保持语法的简洁,并使用解析树访问者和其他遍历器来实现语言应用程序。在本节中,我们将使用众所周知的访问者模式来实现我们的小型计算器。为了让我们的工作更加简便,ANTLR会自动生成访问者接口和空白访问者实现对象。

在我们介绍访问者之前,我们需要对语法进行一些修改。首先,我们需要为规则的不同选择项添加标签。(标签可以是与规则名称不冲突的任何标识符。)如果没有为选择项添加标签,ANTLR只会为每个规则生成一个访问者方法。(第7章《将语法与特定应用程序代码解耦》,第109页使用了类似的语法来更详细地解释访问者机制。)在我们的情况下,我们希望每个选择项有不同的访问者方法,以便我们可以针对每种类型的输入短语获得不同的“事件”。在我们的新语法LabeledExpr中,标签出现在选择项的右侧,并以#符号开头。

接下来,让我们为操作符的字面量定义一些标记名称,这样以后在访问者中可以将标记名称作为Java常量进行引用。

grammar LabeledExpr; // rename to distinguish from Expr.g4

prog:   stat+ ;

stat:   expr NEWLINE                # printExpr
    |   ID '=' expr NEWLINE         # assign
    |   NEWLINE                     # blank
    ;

expr:   expr op=('*'|'/') expr      # MulDiv
    |   expr op=('+'|'-') expr      # AddSub
    |   INT                         # int
    |   ID                          # id
    |   '(' expr ')'                # parens
    ;

MUL :   '*' ; // assigns token name to '*' used above in grammar
DIV :   '/' ;
ADD :   '+' ;
SUB :   '-' ;
ID  :   [a-zA-Z]+ ;      // match identifiers
INT :   [0-9]+ ;         // match integers
NEWLINE:'\r'? '\n' ;     // return newlines to parser (is end-statement signal)
WS  :   [ \t]+ -> skip ; // toss out whitespace

现在我们有了一个经过适当增强的语法,让我们开始编写我们的计算器并看看主程序的样子。我们在文件Calc.java中的主程序几乎与之前的ExprJoyRide.java中的main()函数相同。第一个区别是我们创建的词法分析器和语法分析器对象是基于LabeledExpr语法,而不是Expr。

/***
 * Excerpted from "The Definitive ANTLR 4 Reference",
 * published by The Pragmatic Bookshelf.
 * Copyrights apply to this code. It may not be used to create training material, 
 * courses, books, articles, and the like. Contact us if you are in doubt.
 * We make no guarantees that this code is fit for any purpose. 
 * Visit http://www.pragmaticprogrammer.com/titles/tpantlr2 for more book information.
***/
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.ParseTree;

import java.io.FileInputStream;
import java.io.InputStream;

public class Calc {
    public static void main(String[] args) throws Exception {
        String inputFile = null;
        if ( args.length>0 ) inputFile = args[0];
        InputStream is = System.in;
        if ( inputFile!=null ) is = new FileInputStream(inputFile);
        ANTLRInputStream input = new ANTLRInputStream(is);
        LabeledExprLexer lexer = new LabeledExprLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        LabeledExprParser parser = new LabeledExprParser(tokens);
        ParseTree tree = parser.prog(); // parse

        EvalVisitor eval = new EvalVisitor();
        eval.visit(tree);
    }
}

我们还可以删除显示树形结构文本的打印语句。另一个区别是我们创建了我们的访问者类EvalVisitor的一个实例,我们很快就会介绍。为了开始遍历从方法prog()返回的解析树,我们调用visit()方法。

我们现在已经完成了所有支持的机制。唯一剩下的事情就是实现一个访问者,通过遍历解析树来计算和返回值。为了开始,让我们看看当我们输入时,ANTLR为我们生成了什么代码。

$ antlr4 -no-listener -visitor LabeledExpr.g4

首先,ANTLR会生成一个访问者接口,其中包含每个带有标签的选择项名称对应的方法。

public interface LabeledExprVisitor<T> {
  T visitId(LabeledExprParser.IdContext ctx); # from label id
  T visitAssign(LabeledExprParser.AssignContext ctx); # from label assign 
  T visitMulDiv(LabeledExprParser.MulDivContext ctx); # from label MulDiv 
...
}

接口定义使用了Java泛型,通过参数化类型来表示visit方法的返回值。这样我们可以根据我们要实现的计算来选择返回值类型,并派生出相应的实现类。

接下来,ANTLR会生成一个名为LabeledExprBaseVisitor的默认访问者实现,我们可以对其进行子类化。在这种情况下,我们的表达式结果是整数,因此我们的EvalVisitor应该扩展LabeledExprBaseVisitor。为了实现计算器,我们重写了与语句和表达式选择项相关的方法。以下是完整的代码。您可以选择复制粘贴或保存tour/EvalVisitor的链接(电子书版本)。

/***
 * Excerpted from "The Definitive ANTLR 4 Reference",
 * published by The Pragmatic Bookshelf.
 * Copyrights apply to this code. It may not be used to create training material, 
 * courses, books, articles, and the like. Contact us if you are in doubt.
 * We make no guarantees that this code is fit for any purpose. 
 * Visit http://www.pragmaticprogrammer.com/titles/tpantlr2 for more book information.
***/
import java.util.HashMap;
import java.util.Map;

public class EvalVisitor extends LabeledExprBaseVisitor<Integer> {
    /** "memory" for our calculator; variable/value pairs go here */
    Map<String, Integer> memory = new HashMap<String, Integer>();

    /** ID '=' expr NEWLINE */
    @Override
    public Integer visitAssign(LabeledExprParser.AssignContext ctx) {
        String id = ctx.ID().getText();  // id is left-hand side of '='
        int value = visit(ctx.expr());   // compute value of expression on right
        memory.put(id, value);           // store it in our memory
        return value;
    }

    /** expr NEWLINE */
    @Override
    public Integer visitPrintExpr(LabeledExprParser.PrintExprContext ctx) {
        Integer value = visit(ctx.expr()); // evaluate the expr child
        System.out.println(value);         // print the result
        return 0;                          // return dummy value
    }

    /** INT */
    @Override
    public Integer visitInt(LabeledExprParser.IntContext ctx) {
        return Integer.valueOf(ctx.INT().getText());
    }

    /** ID */
    @Override
    public Integer visitId(LabeledExprParser.IdContext ctx) {
        String id = ctx.ID().getText();
        if ( memory.containsKey(id) ) return memory.get(id);
        return 0;
    }

    /** expr op=('*'|'/') expr */
    @Override
    public Integer visitMulDiv(LabeledExprParser.MulDivContext ctx) {
        int left = visit(ctx.expr(0));  // get value of left subexpression
        int right = visit(ctx.expr(1)); // get value of right subexpression
        if ( ctx.op.getType() == LabeledExprParser.MUL ) return left * right;
        return left / right; // must be DIV
    }

    /** expr op=('+'|'-') expr */
    @Override
    public Integer visitAddSub(LabeledExprParser.AddSubContext ctx) {
        int left = visit(ctx.expr(0));  // get value of left subexpression
        int right = visit(ctx.expr(1)); // get value of right subexpression
        if ( ctx.op.getType() == LabeledExprParser.ADD ) return left + right;
        return left - right; // must be SUB
    }

    /** '(' expr ')' */
    @Override
    public Integer visitParens(LabeledExprParser.ParensContext ctx) {
        return visit(ctx.expr()); // return child expr's value
    }
}

这是评估t.expr中表达式的构建和测试序列:

$ antlr4 -no-listener -visitor LabeledExpr.g4 # -visitor is required!!! 
$ ls LabeledExpr*.java
LabeledExprBaseVisitor.java   LabeledExprParser.java
LabeledExprLexer.java         LabeledExprVisitor.java

$ javac Calc.java LabeledExpr*.java 
$ cat t.expr
193
a=5
b=6
a+b*2
(1+2)*3

$ java Calc t.expr 193
17 
9

要点是,我们构建了一个计算器,而无需在语法中插入原始的Java操作,就像在ANTLR v3中那样需要做的那样。语法保持应用程序无关和编程语言中立。访问者机制还将一切与识别相关的内容保留在熟悉的Java领域中。在构建生成的解析器之上的语言应用程序时,无需学习额外的ANTLR符号。

在继续之前,您可以花些时间尝试通过添加一个clear语句来扩展该表达式语言。这是一个不需要了解所有细节但可以实际做一些事情的好方法。clear命令应该清除内存映射,并且您需要在规则stat中识别它的一个新的替代方案。将该替代方案标记为#clear,然后运行ANTLR对语法进行扩展访问者接口。然后,在visitClear()中实现一些操作以响应clear。按照之前的序列编译和运行Calc。

现在,让我们转变思路,思考翻译而不是评估或解释输入。在下一节中,我们将使用一种名为listener的访问者变体为Java源代码构建一个翻译器。

构建一个具有监听器的翻译器

想象一下,你的老板指派你构建一个工具,该工具可以从Java类定义中生成一个Java接口文件。如果你是一名初级程序员,可能会陷入恐慌。作为一名经验丰富的Java开发者,你可能会建议使用Java反射API或javap工具来提取方法签名。如果你的Java工具构建技巧非常强,甚至可以尝试使用字节码库,比如ASM。然后你的老板说:“哦,是的。在方法签名的范围内保留空格和注释。”现在没有绕过它的办法了。我们必须解析Java源代码。例如,我们希望读取以下这样的Java代码:

/***
 * Excerpted from "The Definitive ANTLR 4 Reference",
 * published by The Pragmatic Bookshelf.
 * Copyrights apply to this code. It may not be used to create training material, 
 * courses, books, articles, and the like. Contact us if you are in doubt.
 * We make no guarantees that this code is fit for any purpose. 
 * Visit http://www.pragmaticprogrammer.com/titles/tpantlr2 for more book information.
***/
import java.util.List;
import java.util.Map;
public class Demo {
	void f(int x, String y) { }
	int[] g(/*no args*/) { return null; }
	List<Map<String, Integer>>[] h() { return null; }
}

并生成一个接口,其中包含方法的签名,同时保留空格和注释。

interface IDemo {
    void f(int x, String y);
    int[] g(/*no args*/); 
    List<Map<String, Integer>>[] h();
}

信不信由你,我们将通过监听从Java解析树遍历器触发的“事件”,在大约15行代码中解决这个问题的核心。Java解析树将来自于从本书源代码中包含的现有Java语法生成的解析器。我们将从类名派生生成的接口名称,并从方法定义中获取方法的签名(返回类型、方法名和参数列表)。有关类似但更详细解释的示例,请参阅第8.3节第134页上的“生成调用图”。

语法和我们的监听器对象之间的关键“接口”称为JavaListener,并且ANTLR会自动生成它。它定义了ANTLR运行时的ParseTreeWalker类在遍历解析树时可以触发的所有方法。在我们的案例中,我们需要通过覆盖三个方法来响应三个事件:当遍历器进入和退出类定义时,以及遇到方法定义时。以下是生成的监听器接口中相关的方法:

public interface JavaListener extends ParseTreeListener {
  void enterClassDeclaration(JavaParser.ClassDeclarationContext ctx);   
  void exitClassDeclaration(JavaParser.ClassDeclarationContext ctx);
  void enterMethodDeclaration(JavaParser.MethodDeclarationContext ctx); 
  ...
}

监听器机制和访问者机制之间最大的区别在于,监听器方法由ANTLR提供的遍历器对象调用,而访问者方法必须使用显式的visit调用遍历它们的子节点。如果忘记在节点的子节点上调用visit(),那么这些子树将不会被访问。 为了构建我们的监听器实现,我们需要了解classDeclaration和methodDeclaration规则的样子,因为监听器方法必须抓取由这些规则匹配的短语元素。Java.g4文件是Java的完整语法,但是对于这个问题,我们需要查看以下两个方法:

    /** Java 1.6 grammar (ANTLR v4). Derived from

    http://docs.oracle.com/javase/specs/jls/se7/jls7.pdf

    and JavaParser.g from ANTLR v3
 */
grammar Java;

@lexer::members {
  protected boolean enumIsKeyword = true;
  protected boolean assertIsKeyword = true;
}

// starting point for parsing a java file
compilationUnit
    :   packageDeclaration? importDeclaration* typeDeclaration*
        EOF
    ;

packageDeclaration
    :   'package' qualifiedName ';'
    ;

importDeclaration
    :   'import' 'static'? qualifiedName ('.' '*')? ';'
    ;

typeDeclaration
    :   classOrInterfaceModifier*
        (   classDeclaration
        |   interfaceDeclaration
        |   enumDeclaration
        )
    |   ';'
    ;

classDeclaration
    :   'class' Identifier typeParameters? ('extends' type)?
        ('implements' typeList)?
        classBody
    ;

enumDeclaration
    :   ENUM Identifier ('implements' typeList)? enumBody
    ;

interfaceDeclaration
    :   normalInterfaceDeclaration
    |   annotationTypeDeclaration
    ;

classOrInterfaceModifier
    :   annotation   // class or interface
    |   'public'     // class or interface
    |   'protected'  // class or interface
    |   'private'    // class or interface
    |   'abstract'   // class or interface
    |   'static'     // class or interface
    |   'final'      // class only -- does not apply to interfaces
    |   'strictfp'   // class or interface
    ;

modifiers
    :   modifier*
    ;

typeParameters
    :   '<' typeParameter (',' typeParameter)* '>'
    ;

typeParameter
    :   Identifier ('extends' typeBound)?
    ;

typeBound
    :   type ('&' type)*
    ;

enumBody
    :   '{' enumConstants? ','? enumBodyDeclarations? '}'
    ;

enumConstants
    :   enumConstant (',' enumConstant)*
    ;

enumConstant
    :   annotations? Identifier arguments? classBody?
    ;

enumBodyDeclarations
    :   ';' (classBodyDeclaration)*
    ;

normalInterfaceDeclaration
    :   'interface' Identifier typeParameters? ('extends' typeList)? interfaceBody
    ;

typeList
    :   type (',' type)*
    ;

classBody
    :   '{' classBodyDeclaration* '}'
    ;

interfaceBody
    :   '{' interfaceBodyDeclaration* '}'
    ;

classBodyDeclaration
    :   ';'
    |   'static'? block
    |   modifiers member
    ;

member
    :   genericMethodDeclaration
    |   methodDeclaration
    |   fieldDeclaration
    |   constructorDeclaration
    |   interfaceDeclaration
    |   classDeclaration
    ;

methodDeclaration
    :   type Identifier formalParameters ('[' ']')* methodDeclarationRest
    |   'void' Identifier formalParameters methodDeclarationRest
    ;

methodDeclarationRest
    :   ('throws' qualifiedNameList)?
        (   methodBody
        |   ';'
        )
    ;

genericMethodDeclaration
    :   typeParameters methodDeclaration
    ;

fieldDeclaration
    :   type variableDeclarators ';'
    ;

constructorDeclaration
    :   typeParameters? Identifier formalParameters
        ('throws' qualifiedNameList)? constructorBody
    ;

interfaceBodyDeclaration
    :   modifiers interfaceMemberDecl
    |   ';'
    ;

interfaceMemberDecl
    :   interfaceMethodOrFieldDecl
    |   interfaceGenericMethodDecl
    |   'void' Identifier voidInterfaceMethodDeclaratorRest
    |   interfaceDeclaration
    |   classDeclaration
    ;

interfaceMethodOrFieldDecl
    :   type Identifier interfaceMethodOrFieldRest
    ;

interfaceMethodOrFieldRest
    :   constantDeclaratorsRest ';'
    |   interfaceMethodDeclaratorRest
    ;

voidMethodDeclaratorRest
    :   formalParameters ('throws' qualifiedNameList)?
        (   methodBody
        |   ';'
        )
    ;

interfaceMethodDeclaratorRest
    :   formalParameters ('[' ']')* ('throws' qualifiedNameList)? ';'
    ;

interfaceGenericMethodDecl
    :   typeParameters (type | 'void') Identifier
        interfaceMethodDeclaratorRest
    ;

voidInterfaceMethodDeclaratorRest
    :   formalParameters ('throws' qualifiedNameList)? ';'
    ;

constantDeclarator
    :   Identifier constantDeclaratorRest
    ;

variableDeclarators
    :   variableDeclarator (',' variableDeclarator)*
    ;

variableDeclarator
    :   variableDeclaratorId ('=' variableInitializer)?
    ;

constantDeclaratorsRest
    :   constantDeclaratorRest (',' constantDeclarator)*
    ;

constantDeclaratorRest
    :   ('[' ']')* '=' variableInitializer
    ;

variableDeclaratorId
    :   Identifier ('[' ']')*
    ;

variableInitializer
    :   arrayInitializer
    |   expression
    ;

arrayInitializer
    :   '{' (variableInitializer (',' variableInitializer)* (',')? )? '}'
    ;

modifier
    :   annotation
    |   'public'
    |   'protected'
    |   'private'
    |   'static'
    |   'abstract'
    |   'final'
    |   'native'
    |   'synchronized'
    |   'transient'
    |   'volatile'
    |   'strictfp'
    ;

packageOrTypeName
    :   qualifiedName
    ;

enumConstantName
    :   Identifier
    ;

typeName
    :   qualifiedName
    ;

type:   classOrInterfaceType ('[' ']')*
    |   primitiveType ('[' ']')*
    ;

classOrInterfaceType
    :   Identifier typeArguments? ('.' Identifier typeArguments? )*
    ;

primitiveType
    :   'boolean'
    |   'char'
    |   'byte'
    |   'short'
    |   'int'
    |   'long'
    |   'float'
    |   'double'
    ;

variableModifier
    :   'final'
    |   annotation
    ;

typeArguments
    :   '<' typeArgument (',' typeArgument)* '>'
    ;

typeArgument
    :   type
    |   '?' (('extends' | 'super') type)?
    ;

qualifiedNameList
    :   qualifiedName (',' qualifiedName)*
    ;

formalParameters
    :   '(' formalParameterDecls? ')'
    ;

formalParameterDecls
    :   variableModifiers type formalParameterDeclsRest
    ;

formalParameterDeclsRest
    :   variableDeclaratorId (',' formalParameterDecls)?
    |   '...' variableDeclaratorId
    ;

methodBody
    :   block
    ;

constructorBody
    :   '{' explicitConstructorInvocation? blockStatement* '}'
    ;

explicitConstructorInvocation
    :   nonWildcardTypeArguments? ('this' | 'super') arguments ';'
    |   primary '.' nonWildcardTypeArguments? 'super' arguments ';'
    ;

qualifiedName
    :   Identifier ('.' Identifier)*
    ;

literal
    :   integerLiteral
    |   FloatingPointLiteral
    |   CharacterLiteral
    |   StringLiteral
    |   booleanLiteral
    |   'null'
    ;

integerLiteral
    :   HexLiteral
    |   OctalLiteral
    |   DecimalLiteral
    ;

booleanLiteral
    :   'true'
    |   'false'
    ;

// ANNOTATIONS

annotations
    :   annotation+
    ;

annotation
    :   '@' annotationName ( '(' ( elementValuePairs | elementValue )? ')' )?
    ;

annotationName
    : Identifier ('.' Identifier)*
    ;

elementValuePairs
    :   elementValuePair (',' elementValuePair)*
    ;

elementValuePair
    :   Identifier '=' elementValue
    ;

elementValue
    :   expression
    |   annotation
    |   elementValueArrayInitializer
    ;

elementValueArrayInitializer
    :   '{' (elementValue (',' elementValue)*)? (',')? '}'
    ;

annotationTypeDeclaration
    :   '@' 'interface' Identifier annotationTypeBody
    ;

annotationTypeBody
    :   '{' (annotationTypeElementDeclaration)* '}'
    ;

annotationTypeElementDeclaration
    :   modifiers annotationTypeElementRest
    ;

annotationTypeElementRest
    :   type annotationMethodOrConstantRest ';'
    |   classDeclaration ';'?
    |   normalInterfaceDeclaration ';'?
    |   enumDeclaration ';'?
    |   annotationTypeDeclaration ';'?
    ;

annotationMethodOrConstantRest
    :   annotationMethodRest
    |   annotationConstantRest
    ;

annotationMethodRest
    :   Identifier '(' ')' defaultValue?
    ;

annotationConstantRest
    :   variableDeclarators
    ;

defaultValue
    :   'default' elementValue
    ;

// STATEMENTS / BLOCKS

block
    :   '{' blockStatement* '}'
    ;

blockStatement
    :   localVariableDeclarationStatement
    |   classDeclaration
    |   interfaceDeclaration
    |   statement
    ;

localVariableDeclarationStatement
    :    localVariableDeclaration ';'
    ;

localVariableDeclaration
    :   variableModifiers type variableDeclarators
    ;

variableModifiers
    :   variableModifier*
    ;

statement
    : block
    |   ASSERT expression (':' expression)? ';'
    |   'if' parExpression statement ('else' statement)?
    |   'for' '(' forControl ')' statement
    |   'while' parExpression statement
    |   'do' statement 'while' parExpression ';'
    |   'try' block
        ( catches 'finally' block
        | catches
        | 'finally' block
        )
    |   'switch' parExpression switchBlock
    |   'synchronized' parExpression block
    |   'return' expression? ';'
    |   'throw' expression ';'
    |   'break' Identifier? ';'
    |   'continue' Identifier? ';'
    |   ';'
    |   statementExpression ';'
    |   Identifier ':' statement
    ;

catches
    :   catchClause (catchClause)*
    ;

catchClause
    :   'catch' '(' formalParameter ')' block
    ;

formalParameter
    :   variableModifiers type variableDeclaratorId
    ;

switchBlock
    :   '{' switchBlockStatementGroup* switchLabel* '}'
    ;

switchBlockStatementGroup
    :   switchLabel+ blockStatement*
    ;

switchLabel
    :   'case' constantExpression ':'
    |   'case' enumConstantName ':'
    |   'default' ':'
    ;

forControl
    :   enhancedForControl
    |   forInit? ';' expression? ';' forUpdate?
    ;

forInit
    :   localVariableDeclaration
    |   expressionList
    ;

enhancedForControl
    :   variableModifiers type Identifier ':' expression
    ;

forUpdate
    :   expressionList
    ;

// EXPRESSIONS

parExpression
    :   '(' expression ')'
    ;

expressionList
    :   expression (',' expression)*
    ;

statementExpression
    :   expression
    ;

constantExpression
    :   expression
    ;

expression
    :   primary
    |   expression '.' Identifier
    |   expression '.' 'this'
    |   expression '.' 'super' '(' expressionList? ')'
    |   expression '.' 'new' Identifier '(' expressionList? ')'
    |   expression '.' 'super' '.' Identifier arguments?
    |   expression '.' explicitGenericInvocation
    |   expression '[' expression ']'
    |   expression '(' expressionList? ')'
    |   expression ('++' | '--')
    |   ('+'|'-'|'++'|'--') expression
    |   ('~'|'!') expression
    |   '(' type ')' expression
    |   'new' creator
    |   expression ('*'|'/'|'%') expression
    |   expression ('+'|'-') expression
    |   expression ('<' '<' | '>' '>' '>' | '>' '>') expression
    |   expression ('<' '=' | '>' '=' | '>' | '<') expression
    |   expression 'instanceof' type
    |   expression ('==' | '!=') expression
    |   expression '&' expression
    |   expression '^' expression
    |   expression '|' expression
    |   expression '&&' expression
    |   expression '||' expression
    |   expression '?' expression ':' expression
    |   expression
        ('^='<assoc=right>
        |'+='<assoc=right>
        |'-='<assoc=right>
        |'*='<assoc=right>
        |'/='<assoc=right>
        |'&='<assoc=right>
        |'|='<assoc=right>
        |'='<assoc=right>
        |'>' '>' '='<assoc=right>
        |'>' '>' '>' '='<assoc=right>
        |'<' '<' '='<assoc=right>
        |'%='<assoc=right>
        )
        expression
    ;

primary
    :   '(' expression ')'
    |   'this'
    |   'super'
    |   literal
    |   Identifier
    |   type '.' 'class'
    |   'void' '.' 'class'
    ;

creator
    :   nonWildcardTypeArguments createdName classCreatorRest
    |   createdName (arrayCreatorRest | classCreatorRest)
    ;

createdName
    :   classOrInterfaceType
    |   primitiveType
    ;

innerCreator
    :   nonWildcardTypeArguments? Identifier classCreatorRest
    ;

explicitGenericInvocation
    :   nonWildcardTypeArguments Identifier arguments
    ;

arrayCreatorRest
    :   '['
        (   ']' ('[' ']')* arrayInitializer
        |   expression ']' ('[' expression ']')* ('[' ']')*
        )
    ;

classCreatorRest
    :   arguments classBody?
    ;

nonWildcardTypeArguments
    :   '<' typeList '>'
    ;

arguments
    :   '(' expressionList? ')'
    ;

// LEXER

HexLiteral : '0' ('x'|'X') HexDigit+ IntegerTypeSuffix? ;

DecimalLiteral : ('0' | '1'..'9' '0'..'9'*) IntegerTypeSuffix? ;

OctalLiteral : '0' ('0'..'7')+ IntegerTypeSuffix? ;

fragment
HexDigit : ('0'..'9'|'a'..'f'|'A'..'F') ;

fragment
IntegerTypeSuffix : ('l'|'L') ;

FloatingPointLiteral
    :   ('0'..'9')+ '.' ('0'..'9')* Exponent? FloatTypeSuffix?
    |   '.' ('0'..'9')+ Exponent? FloatTypeSuffix?
    |   ('0'..'9')+ Exponent FloatTypeSuffix?
    |   ('0'..'9')+ FloatTypeSuffix
    |   ('0x' | '0X') (HexDigit )*
        ('.' (HexDigit)*)?
        ( 'p' | 'P' )
        ( '+' | '-' )?
        ( '0' .. '9' )+
        FloatTypeSuffix?
    ;

fragment
Exponent : ('e'|'E') ('+'|'-')? ('0'..'9')+ ;

fragment
FloatTypeSuffix : ('f'|'F'|'d'|'D') ;

CharacterLiteral
    :   '\'' ( EscapeSequence | ~('\''|'\\') ) '\''
    ;

StringLiteral
    :  '"' ( EscapeSequence | ~('\\'|'"') )* '"'
    ;

fragment
EscapeSequence
    :   '\\' ('b'|'t'|'n'|'f'|'r'|'\"'|'\''|'\\')
    |   UnicodeEscape
    |   OctalEscape
    ;

fragment
OctalEscape
    :   '\\' ('0'..'3') ('0'..'7') ('0'..'7')
    |   '\\' ('0'..'7') ('0'..'7')
    |   '\\' ('0'..'7')
    ;

fragment
UnicodeEscape
    :   '\\' 'u' HexDigit HexDigit HexDigit HexDigit
    ;

ENUM:   'enum' {if (!enumIsKeyword) setType(Identifier);}
    ;

ASSERT
    :   'assert' {if (!assertIsKeyword) setType(Identifier);}
    ;

Identifier
    :   Letter (Letter|JavaIDDigit)*
    ;

/**I found this char range in JavaCC's grammar, but Letter and Digit overlap.
   Still works, but...
 */
fragment
Letter
    :  '\u0024' |
       '\u0041'..'\u005a' |
       '\u005f' |
       '\u0061'..'\u007a' |
       '\u00c0'..'\u00d6' |
       '\u00d8'..'\u00f6' |
       '\u00f8'..'\u00ff' |
       '\u0100'..'\u1fff' |
       '\u3040'..'\u318f' |
       '\u3300'..'\u337f' |
       '\u3400'..'\u3d2d' |
       '\u4e00'..'\u9fff' |
       '\uf900'..'\ufaff'
    ;

fragment
JavaIDDigit
    :  '\u0030'..'\u0039' |
       '\u0660'..'\u0669' |
       '\u06f0'..'\u06f9' |
       '\u0966'..'\u096f' |
       '\u09e6'..'\u09ef' |
       '\u0a66'..'\u0a6f' |
       '\u0ae6'..'\u0aef' |
       '\u0b66'..'\u0b6f' |
       '\u0be7'..'\u0bef' |
       '\u0c66'..'\u0c6f' |
       '\u0ce6'..'\u0cef' |
       '\u0d66'..'\u0d6f' |
       '\u0e50'..'\u0e59' |
       '\u0ed0'..'\u0ed9' |
       '\u1040'..'\u1049'
   ;

COMMENT
    :   '/*' .*? '*/'    -> channel(HIDDEN) // match anything between /* and */
    ;
WS  :   [ \r\t\u000C\n]+ -> channel(HIDDEN)
    ;

LINE_COMMENT
    : '//' ~[\r\n]* '\r'? '\n' -> channel(HIDDEN)
    ;

为了不必实现所有200多个接口方法,ANTLR生成了一个名为JavaBaseListener的默认实现。然后,我们的接口提取器可以继承JavaBaseListener并重写感兴趣的方法。 我们的基本策略是,在看到类定义的开始时打印出接口头部。然后,在类定义的末尾打印出一个终止的}。在每个方法定义上,我们将输出其签名。以下是完整的实现:

/***
 * Excerpted from "The Definitive ANTLR 4 Reference",
 * published by The Pragmatic Bookshelf.
 * Copyrights apply to this code. It may not be used to create training material, 
 * courses, books, articles, and the like. Contact us if you are in doubt.
 * We make no guarantees that this code is fit for any purpose. 
 * Visit http://www.pragmaticprogrammer.com/titles/tpantlr2 for more book information.
***/
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.misc.Interval;

public class ExtractInterfaceListener extends JavaBaseListener {
    JavaParser parser;
    public ExtractInterfaceListener(JavaParser parser) {this.parser = parser;}
    /** Listen to matches of classDeclaration */
    @Override
    public void enterClassDeclaration(JavaParser.ClassDeclarationContext ctx){
        System.out.println("interface I"+ctx.Identifier()+" {");
    }
    @Override
    public void exitClassDeclaration(JavaParser.ClassDeclarationContext ctx) {
        System.out.println("}");
    }

    /** Listen to matches of methodDeclaration */
    @Override
    public void enterMethodDeclaration(
        JavaParser.MethodDeclarationContext ctx
    )
    {
        // need parser to get tokens
        TokenStream tokens = parser.getTokenStream();
        String type = "void";
        if ( ctx.type()!=null ) {
            type = tokens.getText(ctx.type());
        }
        String args = tokens.getText(ctx.formalParameters());
        System.out.println("\t"+type+" "+ctx.Identifier()+args+";");
    }
}

要启动这个程序,我们需要一个主程序,它与本章中的其他程序几乎相同。我们的应用程序代码在启动解析器之后开始执行。

/***
 * Excerpted from "The Definitive ANTLR 4 Reference",
 * published by The Pragmatic Bookshelf.
 * Copyrights apply to this code. It may not be used to create training material, 
 * courses, books, articles, and the like. Contact us if you are in doubt.
 * We make no guarantees that this code is fit for any purpose. 
 * Visit http://www.pragmaticprogrammer.com/titles/tpantlr2 for more book information.
***/
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.*;

import java.io.FileInputStream;
import java.io.InputStream;

public class ExtractInterfaceTool {
    public static void main(String[] args) throws Exception {
        String inputFile = null;
        if ( args.length>0 ) inputFile = args[0];
        InputStream is = System.in;
        if ( inputFile!=null ) {
            is = new FileInputStream(inputFile);
        }
        ANTLRInputStream input = new ANTLRInputStream(is);

        JavaLexer lexer = new JavaLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        JavaParser parser = new JavaParser(tokens);
        ParseTree tree = parser.compilationUnit(); // parse

        ParseTreeWalker walker = new ParseTreeWalker(); // create standard walker
        ExtractInterfaceListener extractor = new ExtractInterfaceListener(parser);
        walker.walk(extractor, tree); // initiate walk of tree with listener
    }
}