1. 概要

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

2. ANTLR

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

これは、レクサー、文法、パーサーなどの言語処理プリミティブへのアクセスと、それらに対してテキストを処理するためのランタイムを提供することによって行われます。

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

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

3. 構成

まず、antlr-runtimepom.xmlに追加することから始めましょう。

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

また、 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. それはどのように機能しますか?

基本的に、 ANTLR Mavenプラグインを使用してパーサーを作成する場合は、次の3つの簡単な手順に従う必要があります。

  • 文法ファイルを準備する
  • ソースを生成する
  • リスナーを作成する

それでは、これらの手順を実際に見てみましょう。

5. 既存の文法を使用する

まず、ANTLRを使用して、大文字と小文字が正しくないメソッドのコードを分析しましょう。

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

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

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

素晴らしいのは、私たちの目的に合う文法ファイルがすでにいくつかあることです。

ANTLRのGithub文法リポジトリで見つけたJava8.g4文法ファイルを使用してみましょう。

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

5.2. ソースを生成する

ANTLRは、提供する文法ファイルに対応するJavaコードを生成することで機能し、mavenプラグインを使用すると次のことが簡単になります。

mvn package

デフォルトでは、これにより target /generated-sources /antlr4ディレクトリの下にいくつかのファイルが生成されます。

  • Java8.interp
  • Java8Listener.java
  • Java8BaseListener.java
  • Java8Lexer.java
  • Java8Lexer.interp
  • Java8Parser.java
  • Java8.tokens
  • Java8Lexer.tokens

これらのファイルの名前は、文法ファイルの名前に基づいていることに注意してください。

後でテストするときに、Java8LexerファイルとJava8Parserファイルが必要になります。 ただし、今のところ、MethodUppercaseListenerを作成するにはJava8BaseListenerが必要です。

5.3. MethodUppercaseListenerの作成

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

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

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

したがって、 Java8BaseListener には、このパターンが検出されるたびに呼び出されるメソッドenterMethodDeclaratorがあります。

それでは、 enterMethodDeclarator をオーバーライドし、 Identity を引き出して、チェックを実行しましょう。

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 : DATE ' ' TIME;

レベルの場合:

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

そしてメッセージの場合:

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、を入力するたびに再初期化できます。

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

次に、 enterTimestamp enterLevel、、および enterMessage を使用して、適切なLogEntryプロパティを設定します。

    @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()));
    }

最後に、 exitEntry メソッドを使用して、新しいLogEntryを作成して追加しましょう。

    @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を使用して自国語のカスタムパーサーを作成する方法に焦点を当てました。

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

いつものように、ここで使用されるすべてのコードは、GitHubにあります。