Java avec ANTLR

1. Vue d'ensemble

Dans ce didacticiel, nous allons faire un aperçu rapide du générateur d'analyseur ANTLR et montrer quelques applications du monde réel.

2. ANTLR

ANTLR (ANother Tool for Language Recognition) est un outil de traitement de texte structuré.

Il le fait en nous donnant accès aux primitives de traitement du langage comme les lexers, les grammaires et les analyseurs, ainsi qu'au runtime pour traiter le texte contre eux.

Il est souvent utilisé pour créer des outils et des cadres. Par exemple, Hibernate utilise ANTLR pour analyser et traiter les requêtes HQL et Elasticsearch l'utilise pour Indolore.

Et Java n'est qu'une liaison. ANTLR propose également des liaisons pour C #, Python, JavaScript, Go, C ++ et Swift.

3. Configuration

Tout d'abord, commençons par ajouter antlr-runtime à notre pom.xml :

 org.antlr antlr4-runtime 4.7.1 

Et aussi l'antlr-maven-plugin:

 org.antlr antlr4-maven-plugin 4.7.1    antlr4    

C'est le travail du plugin de générer du code à partir des grammaires que nous spécifions.

4. Comment ça marche?

Fondamentalement, lorsque nous voulons créer l'analyseur en utilisant le plugin ANTLR Maven, nous devons suivre trois étapes simples:

  • préparer un fichier de grammaire
  • générer des sources
  • créer l'auditeur

Alors, voyons ces étapes en action.

5. Utilisation d'une grammaire existante

Utilisons d'abord ANTLR pour analyser le code des méthodes avec une mauvaise casse:

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

En termes simples, nous validerons que tous les noms de méthodes de notre code commencent par une lettre minuscule.

5.1. Préparer un fichier de grammaire

Ce qui est bien, c'est qu'il existe déjà plusieurs fichiers de grammaire qui peuvent répondre à nos besoins.

Utilisons le fichier de grammaire Java8.g4 que nous avons trouvé dans le référentiel de grammaire Github d'ANTLR.

Nous pouvons créer le répertoire src / main / antlr4 et le télécharger ici.

5.2. Générer des sources

ANTLR fonctionne en générant du code Java correspondant aux fichiers de grammaire que nous lui donnons, et le plugin maven facilite les choses:

mvn package

Par défaut, cela générera plusieurs fichiers dans le répertoire target / generated-sources / antlr4 :

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

Notez que les noms de ces fichiers sont basés sur le nom du fichier de grammaire .

Nous aurons besoin des fichiers Java8Lexer et Java8Parser plus tard lors du test. Pour l'instant, cependant, nous avons besoin du Java8BaseListener pour créer notre MethodUppercaseListener .

5.3. Création de MethodUppercaseListener

Basé sur la grammaire Java8 que nous avons utilisée, Java8BaseListener a plusieurs méthodes que nous pouvons remplacer, chacune correspondant à un en-tête dans le fichier de grammaire.

Par exemple, la grammaire définit le nom de la méthode, la liste des paramètres et la clause throws comme ceci:

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

Et donc Java8BaseListener a une méthode enterMethodDeclarator qui sera appelée à chaque fois que ce modèle est rencontré.

Alors, remplaçons enterMethodDeclarator , retirons l' identificateur et effectuons notre vérification:

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. Essai

Maintenant, faisons quelques tests. Tout d'abord, nous construisons le lexer:

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

Ensuite, nous instancions l'analyseur:

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

Et puis, le marcheur et l'auditeur:

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

Enfin, nous disons à ANTLR de parcourir notre exemple de classe :

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

6. Construire notre grammaire

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

Dans cet article, nous nous sommes concentrés sur la façon de créer l'analyseur personnalisé pour le propre langage à l'aide de l'ANTLR.

Nous avons également vu comment utiliser des fichiers de grammaire existants et les appliquer pour des tâches très simples comme le linting de code.

Comme toujours, tout le code utilisé ici peut être trouvé sur GitHub.