1概要

このチュートリアルでは、http://www.antlr.org/[ANTLR]パーサジェネレータの概要を説明し、実際のアプリケーションをいくつか紹介します。


2 ANTLR

ANTLR(言語認識のためのANotherツール)は構造化テキストを処理するためのツールです。

これは、レクサー、文法、パーサーなどの言語処理プリミティブ、およびそれらに対してテキストを処理するためのランタイムにアクセスできるようにすることで実現します。

ツールやフレームワークを構築するためによく使用されます。たとえば、HibernateはHQLクエリの解析と処理にANTLRを使用し、ElasticsearchはPainlessにそれを使用します。

そしてJavaはただ一つの束縛です。 ANTLRは、C#、Python、JavaScript、Go、C、およびSwift用のバインディングも提供しています。


3構成

まず、https://search.maven.org/classic/#search%7C1%7Cg%3A%22org.antlr%22%20AND%20a%3A%22antlr4-runtime%22[antlrを追加することから始めましょう[ランタイム]を__pom.xmlに変更します。

<dependency>
    <groupId>org.antlr</groupId>
    <artifactId>antlr4-runtime</artifactId>
    <version>4.7.1</version>
</dependency>

そしてまたhttps://search.maven.org/classic/#search%7Cga%7C1%7Corg.antlr%20antlr-maven-plugin[antlr-maven-plugin]:

<plugin>
    <groupId>org.antlr</groupId>
    <artifactId>antlr4-maven-plugin</artifactId>
    <version>4.7.1</version>
    <executions>
        <execution>
            <goals>
                <goal>antlr4</goal>
            </goals>
        </execution>
    </executions>
</plugin>

指定した文法からコードを生成するのはプラグインの仕事です。


4どのように動作しますか?

基本的に、http://www.antlr.org/api/maven-plugin/latest/[ANTLR Mavenプラグイン]を使用してパーサーを作成したい場合は、3つの簡単なステップに従う必要があります。

文法ファイルを用意する


  • ソースを生成する


  • リスナーを作成する

それでは、これらのステップを実際に見てみましょう。


5既存の文法を使う

まず、ANTLRを使用して、大文字小文字の区別がないメソッドのコードを分析します。

public class SampleClass {

    public void DoSomethingElse() {
       //...
    }
}

簡単に言えば、コード内のすべてのメソッド名が小文字で始まることを検証します。


5.1. 文法ファイルを準備する

私たちの目的に合った文法ファイルが既にいくつかあります。


Java8.g4の文法ファイル

を使用しましょう。[ANTLRのGithub文法レポ]。


src/main/antlr4

ディレクトリを作成してそこにダウンロードできます。


5.2. ソースを生成

ANTLRは、与えられた文法ファイルに対応するJavaコードを生成することによって機能します。そしてmavenプラグインはそれを簡単にします。

mvn package

デフォルトでは、これは

target/generated-sources/antlr4

ディレクトリの下にいくつかのファイルを生成します。


  • Java8.interp

  • Java Listener.java


  • Java8BaseListener.java

  • Java Lexer.java


  • Java8Lexer.interp

  • Java Parser.java


  • Java8.tokens


  • Java8Lexer.tokens

これらのファイルの名前は、文法ファイルの名前に基づいています。

テストするときには、

__Java8Lexer




Java8Parser

ファイルが必要です。ただし今のところ、

MethodUppercaseListener

を作成するには

Java8BaseListener__が必要です。


5.3.

MethodUppercaseListener


を作成する

使用したJava8の文法に基づいて、

Java8BaseListener

にはオーバーライドできるいくつかのメソッドがあり、それぞれが文法ファイルの見出しに対応しています。

たとえば、文法はメソッド名、パラメータリスト、およびthrows節を次のように定義します。

methodDeclarator
    :   Identifier '(' formalParameterList? ')' dims?
    ;

したがって、

__Java8BaseListener


メソッドには


enterMethodDeclarator

__whichがあり、このパターンが見つかるたびに呼び出されます。

それでは、

enterMethodDeclarator

をオーバーライドし、

Identifier

を引き出して、チェックを実行しましょう。

public class UppercaseMethodListener extends Java8BaseListener {

    private List<String> errors = new ArrayList<>();

   //... getter for errors

    @Override
    public void enterMethodDeclarator(Java8Parser.MethodDeclaratorContext ctx) {
        TerminalNode node = ctx.Identifier();
        String methodName = node.getText();

        if (Character.isUpperCase(methodName.charAt(0))) {
            String error = String.format("Method %s is uppercased!", methodName);
            errors.add(error);
        }
    }
}


5.4. テスト中

それでは、テストをしましょう。まず、字句解析プログラムを構築します。

String javaClassContent = "public class SampleClass { void DoSomething(){} }";
Java8Lexer java8Lexer = new Java8Lexer(CharStreams.fromString(javaClassContent));

次に、パーサーをインスタンス化します。

CommonTokenStream tokens = new CommonTokenStream(lexer);
Java8Parser parser = new Java8Parser(tokens);
ParseTree tree = parser.compilationUnit();

そして、ウォーカーとリスナーは、

ParseTreeWalker walker = new ParseTreeWalker();
UppercaseMethodListener listener= new UppercaseMethodListener();

最後に、ANTLRにサンプルクラス____を見ていくように伝えます。

walker.walk(listener, tree);

assertThat(listener.getErrors().size(), is(1));
assertThat(listener.getErrors().get(0),
  is("Method DoSomething is uppercased!"));


6. 文法の構築

それでは、ログファイルの解析など、もう少し複雑なものを試してみましょう。

2018-May-05 14:20:18 INFO some error occurred
2018-May-05 14:20:19 INFO yet another error
2018-May-05 14:20:20 INFO some method started
2018-May-05 14:20:21 DEBUG another method started
2018-May-05 14:20:21 DEBUG entering awesome method
2018-May-05 14:20:24 ERROR Bad thing happened

カスタムログ形式があるため、最初に独自の文法を作成する必要があります。


6.1. 文法ファイルを準備する

まず、各ログ行がファイル内でどのように見えるかのメンタルマップを作成できるかどうかを確認しましょう。


<日付> <レベル> <メッセージ>

もう1つ深くなれば、次のようになります。


<日付>:= <年> <日> <月> <日> <日> …​

等々。これを考慮することが重要です。そうすれば、テキストをどのレベルの粒度で解析したいのかを判断できます。

文法ファイルは基本的にはレクサーとパーサーの規則の集まりです。簡単に言うと、字句解析規則は文法の構文を記述し、文法規則は意味を記述します。

レクサールールの再利用可能な構成要素であるフラグメントを定義することから始めましょう。

fragment DIGIT :[0-9];
fragment TWODIGIT : DIGIT DIGIT;
fragment LETTER :[A-Za-z];

次に、残り字句解析ルールを定義しましょう。

DATE : TWODIGIT TWODIGIT '-' LETTER LETTER LETTER '-' TWODIGIT;
TIME : TWODIGIT ':' TWODIGIT ':' TWODIGIT;
TEXT   : LETTER+ ;
CRLF : '\r'? '\n' | '\r';

これらのビルディングブロックを配置したら、基本構造用のパーサールールを構築できます。

log : entry+;
entry : timestamp ' ' level ' ' message CRLF;

それから

timestamp

の詳細を追加します。

timestamp : DATE ' ' TIME;


level

の場合:

level : 'ERROR' | 'INFO' | 'DEBUG';

そして

message

の場合:

message : (TEXT | ' ')+;

以上です!私達の文法は使用する準備ができています。前と同じように

src/main/antlr4

ディレクトリの下に置きます。


6.2.


ソースを生成

これは単なる素早い

mvnパッケージ

であり、これが私たちの文法の名前に基づいて

LogBaseListener



LogParser

などのようないくつかのファイルを作成することを思い出してください。


6.3. ログリスナを作成する

これで、リスナーを実装する準備ができました。リスナーは、最終的にログファイルをJavaオブジェクトに解析するために使用します。

それでは、ログエントリのための簡単なモデルクラスから始めましょう:

public class LogEntry {

    private LogLevel level;
    private String message;
    private LocalDateTime timestamp;

   //getters and setters
}

それでは、以前と同じように

LogBaseListener

をサブクラス化する必要があります。

public class LogListener extends LogBaseListener {

    private List<LogEntry> entries = new ArrayList<>();
    private LogEntry current;



_current


は現在のログ行を保持します。文法に基づいて


logEntry、

_

againを入力するたびに再初期化できます。

    @Override
    public void enterEntry(LogParser.EntryContext ctx) {
        this.current = new LogEntry();
    }

次に、適切な

LogEntry

プロパティを設定するために

enterTimestamp



enterLevel、

、および

enterMessage

を使用します。

    @Override
    public void enterTimestamp(LogParser.TimestampContext ctx) {
        this.current.setTimestamp(
          LocalDateTime.parse(ctx.getText(), DEFAULT__DATETIME__FORMATTER));
    }

    @Override
    public void enterMessage(LogParser.MessageContext ctx) {
        this.current.setMessage(ctx.getText());
    }

    @Override
    public void enterLevel(LogParser.LevelContext ctx) {
        this.current.setLevel(LogLevel.valueOf(ctx.getText()));
    }

そして最後に、新しい

LogEntry

を作成および追加するために____exitEntryメソッドを使用しましょう。

    @Override
    public void exitLogEntry(LogParser.EntryContext ctx) {
        this.entries.add(this.current);
    }

  • ところで、私たちの____LogListenerはスレッドセーフではありません!**


6.4. テスト中

そして今度は前回と同じようにもう一度テストします。

@Test
public void whenLogContainsOneErrorLogEntry__thenOneErrorIsReturned()
  throws Exception {

    String logLine ="2018-May-05 14:20:24 ERROR Bad thing happened";

   //instantiate the lexer, the parser, and the walker
    LogListener listener = new LogListener();
    walker.walk(listener, logParser.log());
    LogEntry entry = listener.getEntries().get(0);

    assertThat(entry.getLevel(), is(LogLevel.ERROR));
    assertThat(entry.getMessage(), is("Bad thing happened"));
    assertThat(entry.getTimestamp(), is(LocalDateTime.of(2018,5,5,14,20,24)));
}


7. 結論

この記事では、ANTLRを使用して自国語用のカスタム・パーサーを作成する方法に焦点を当てました。

また、既存の文法ファイルを使用し、それらをコードリンティングなどの非常に単純なタスクに適用する方法も説明しました。

いつものように、ここで使われているすべてのコードはhttps://github.com/eugenp/tutorials/tree/master/antlr[over on GitHub]にあります。