Java dengan ANTLR

1. Ikhtisar

Dalam tutorial ini, kami akan melakukan tinjauan singkat tentang generator parser ANTLR dan menampilkan beberapa aplikasi dunia nyata.

2. ANTLR

ANTLR (ANother Tool for Language Recognition) adalah alat untuk memproses teks terstruktur.

Ini dilakukan dengan memberi kita akses ke primitif pemrosesan bahasa seperti lexers, grammars, dan parser serta runtime untuk memproses teks terhadapnya.

Ini sering digunakan untuk membangun alat dan kerangka kerja. Misalnya, Hibernate menggunakan ANTLR untuk mem-parsing dan memproses kueri HQL dan Elasticsearch menggunakannya untuk Painless.

Dan Java hanyalah satu pengikatan. ANTLR juga menawarkan binding untuk C #, Python, JavaScript, Go, C ++ dan Swift.

3. Konfigurasi

Pertama-tama, mari kita mulai dengan menambahkan antlr-runtime ke pom.xml kita :

 org.antlr antlr4-runtime 4.7.1 

Dan juga antlr-maven-plugin:

 org.antlr antlr4-maven-plugin 4.7.1    antlr4    

Itu tugas plugin untuk menghasilkan kode dari tata bahasa yang kita tentukan.

4. Bagaimana cara kerjanya?

Pada dasarnya, ketika kita ingin membuat parser dengan menggunakan plugin ANTLR Maven, kita perlu mengikuti tiga langkah sederhana:

  • siapkan file tata bahasa
  • menghasilkan sumber
  • buat pendengar

Jadi, mari kita lihat langkah-langkah ini beraksi.

5. Menggunakan Tata Bahasa yang Ada

Mari pertama-tama gunakan ANTLR untuk menganalisis kode untuk metode dengan casing yang buruk:

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

Sederhananya, kami akan memvalidasi bahwa semua nama metode dalam kode kami dimulai dengan huruf kecil.

5.1. Siapkan File Tata Bahasa

Yang bagus adalah sudah ada beberapa file tata bahasa di luar sana yang sesuai dengan tujuan kita.

Mari gunakan file tata bahasa Java8.g4 yang kami temukan di repo tata bahasa Github ANTLR.

Kita dapat membuat direktori src / main / antlr4 dan mendownloadnya di sana.

5.2. Hasilkan Sumber

ANTLR bekerja dengan menghasilkan kode Java yang sesuai dengan file tata bahasa yang kami berikan, dan plugin maven membuatnya mudah:

mvn package

Secara default, ini akan menghasilkan beberapa file di bawah direktori target / generated-sources / antlr4 :

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

Perhatikan bahwa nama file tersebut didasarkan pada nama file tata bahasa .

Kami akan membutuhkan file Java8Lexer dan Java8Parser nanti saat kami menguji. Untuk saat ini, kami membutuhkan Java8BaseListener untuk membuat MethodUppercaseListener kami .

5.3. Membuat MethodUppercaseListener

Berdasarkan tata bahasa Java8 yang kami gunakan, Java8BaseListener memiliki beberapa metode yang dapat kami timpa, masing-masing sesuai dengan judul dalam file tata bahasa.

Misalnya, tata bahasa mendefinisikan nama metode, daftar parameter, dan klausa lemparan seperti ini:

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

Jadi Java8BaseListener memiliki metode enterMethodDeclarator yang akan dipanggil setiap kali pola ini ditemukan.

Jadi, mari kita ganti enterMethodDeclarator , cabut Identifiernya , dan lakukan pemeriksaan kita:

public class UppercaseMethodListener extends Java8BaseListener { private List 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. Menguji

Sekarang, mari kita lakukan beberapa pengujian. Pertama, kami membangun lexer:

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

Kemudian, kami membuat contoh parser:

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

Dan kemudian, pejalan dan pendengar:

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

Terakhir, kami memberi tahu ANTLR untuk menelusuri kelas sampel kami :

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

6. Membangun Tata Bahasa Kami

Now, let's try something just a little bit more complex, like parsing log files:

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

Because we have a custom log format, we're going to first need to create our own grammar.

6.1. Prepare a Grammar File

First, let's see if we can create a mental map of what each log line looks like in our file.

Or if we go one more level deep, we might say:

:= …

And so on. It's important to consider this so we can decide at what level of granularity we want to parse the text.

A grammar file is basically a set of lexer and parser rules. Simply put, lexer rules describe the syntax of the grammar while parser rules describe the semantics.

Let's start by defining fragments which are reusable building blocks for lexer rules.

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

Next, let's define the remainings lexer rules:

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

With these building blocks in place, we can build parser rules for the basic structure:

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

And then we'll add the details for timestamp:

timestamp : DATE ' ' TIME;

For level:

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

And for message:

message : (TEXT | ' ')+;

And that's it! Our grammar is ready to use. We will put it under the src/main/antlr4 directory as before.

6.2.Generate Sources

Recall that this is just a quick mvn package, and that this will create several files like LogBaseListener, LogParser, and so on, based on the name of our grammar.

6.3. Create Our Log Listener

Now, we are ready to implement our listener, which we'll ultimately use to parse a log file into Java objects.

So, let's start with a simple model class for the log entry:

public class LogEntry { private LogLevel level; private String message; private LocalDateTime timestamp; // getters and setters }

Now, we need to subclass LogBaseListener as before:

public class LogListener extends LogBaseListener { private List entries = new ArrayList(); private LogEntry current;

current will hold onto the current log line, which we can reinitialize each time we enter a logEntry, again based on our grammar:

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

Next, we'll use enterTimestamp, enterLevel, and enterMessage for setting the appropriate LogEntry properties:

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

And finally, let's use the exitEntry method in order to create and add our new LogEntry:

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

Note, by the way, that our LogListener isn't threadsafe!

6.4. Testing

And now we can test again as we did last time:

@Test public void whenLogContainsOneErrorLogEntry_thenOneErrorIsReturned() throws Exception { String logLine; // 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. Conclusion

Pada artikel ini, kami fokus pada cara membuat parser kustom untuk bahasa sendiri menggunakan ANTLR.

Kami juga melihat cara menggunakan file tata bahasa yang ada dan menerapkannya untuk tugas yang sangat sederhana seperti linting kode.

Seperti biasa, semua kode yang digunakan di sini dapat ditemukan di GitHub.