Java avec ANTLR

Java avec ANTLR

1. Vue d'ensemble

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

2. ANTLR

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

Pour ce faire, il nous donne accès aux primitives de traitement du langage telles que les lexeurs, les grammaires et les analyseurs ainsi que le temps d’exécution permettant de traiter du texte.

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

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

3. Configuration

Tout d'abord, commençons par ajouterantlr-runtime à nospom.xml:


    org.antlr
    antlr4-runtime
    4.7.1

Et aussi lesantlr-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 lesANTLR Maven plugin, nous devons suivre trois étapes simples:

  • préparer un fichier de grammaire

  • générer des sources

  • créer l'auditeur

Voyons donc ces étapes en action.

5. Utiliser une grammaire existante

Commençons par utiliser ANTLR pour analyser le code des méthodes avec une casse incorrecte:

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.

Utilisonsthe Java8.g4 grammar file que nous avons trouvé dansANTLR’s Github grammar repo.

Nous pouvons créer le répertoiresrc/main/antlr4 et le télécharger là-bas.

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épertoiretarget/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 fichiersare based on the name of the grammar file.

Nous aurons besoin du ponçageJava8Lexer des fichiersJava8Parser plus tard lors du test. Pour l'instant, cependant, nous avons besoin desJava8BaseListener pour créer nosMethodUppercaseListener.

5.3. Création deMethodUppercaseListener

Basé sur la grammaire Java8 que nous avons utilisée,Java8BaseListener dispose de 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 de la manière suivante:

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

Et doncJava8BaseListener utilise une méthodeenterMethodDeclarator qui sera appelée à chaque fois que ce modèle est rencontré.

Alors, remplaçonsenterMethodDeclarator, sortons lesIdentifier 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 demandons à 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

Maintenant, essayons quelque chose d'un peu plus complexe, comme l'analyse des fichiers journaux:

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

Étant donné que nous avons un format de journal personnalisé, nous allons d'abord devoir créer notre propre grammaire.

6.1. Préparer un fichier de grammaire

Tout d'abord, voyons si nous pouvons créer une carte mentale de ce à quoi ressemble chaque ligne de journal dans notre fichier.

Ou si nous atteignons un niveau plus profond, nous pourrions dire:

: =

Etc. Il est important de tenir compte de cela afin que nous puissions décider à quel niveau de granularité nous voulons analyser le texte.

Un fichier de grammaire est essentiellement un ensemble de règles de lexer et d’analyseur. Simply put, lexer rules describe the syntax of the grammar while parser rules describe the semantics.

Commençons par définir les fragments qui sontreusable building blocks for lexer rules.

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

Ensuite, définissons les règles de lexer restantes:

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

Avec ces blocs de construction en place, nous pouvons créer des règles d’analyseur pour la structure de base:

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

Et puis nous ajouterons les détails pourtimestamp:

timestamp : DATE ' ' TIME;

Pourlevel:

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

Et pourmessage:

message : (TEXT | ' ')+;

Et c'est tout! Notre grammaire est prête à être utilisée. Nous le placerons dans le répertoiresrc/main/antlr4 comme précédemment.

6.2. Générer des sources

Rappelez-vous qu'il ne s'agit que d'un rapidemvn package, et que cela créera plusieurs fichiers commeLogBaseListener,LogParser, etc., en fonction du nom de notre grammaire.

6.3. Créez notre écouteur de journal

Nous sommes maintenant prêts à implémenter notre écouteur, que nous utiliserons finalement pour analyser un fichier journal en objets Java.

Commençons donc par une classe de modèle simple pour l'entrée de journal:

public class LogEntry {

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

    // getters and setters
}

Maintenant, nous devons sous-classerLogBaseListener comme avant:

public class LogListener extends LogBaseListener {

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

current will retient la ligne de journal actuelle, que nous pouvons réinitialiser à chaque fois que nous entrons unlogEntry, again basé sur notre grammaire:

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

Ensuite, nous utiliseronsenterTimestamp,enterLevel, etenterMessage pour définir les propriétésLogEntry appropriées:

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

Et enfin, utilisons la méthodeexitEntry pour créer et ajouter nos nouveauxLogEntry:

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

Notez, d’ailleurs, que notreLogListener  n’est pas threadsafe!

6.4. Essai

Et maintenant, nous pouvons tester à nouveau comme nous l'avons fait la dernière fois:

@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. Conclusion

Dans cet article, nous nous sommes concentrés sur la création d'un analyseur personnalisé pour le propre langage à l'aide de ANTLR.

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

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