Java met ANTLR

1. Overzicht

In deze tutorial geven we een kort overzicht van de ANTLR-parser-generator en laten we enkele real-world applicaties zien.

2. ANTLR

ANTLR (ANother Tool for Language Recognition) is een tool voor het verwerken van gestructureerde tekst.

Het doet dit door ons toegang te geven tot taalverwerkingsprimitieven zoals lexers, grammatica's en parsers, evenals de runtime om tekst tegen hen te verwerken.

Het wordt vaak gebruikt om tools en frameworks te bouwen. Hibernate gebruikt bijvoorbeeld ANTLR voor het parseren en verwerken van HQL-query's en Elasticsearch gebruikt het voor pijnloos.

En Java is slechts één binding. ANTLR biedt ook bindingen voor C #, Python, JavaScript, Go, C ++ en Swift.

3. Configuratie

Laten we allereerst beginnen met het toevoegen van antlr-runtime aan ons pom.xml:

 org.antlr antlr4-runtime 4.7.1 

En ook de antlr-maven-plugin:

 org.antlr antlr4-maven-plugin 4.7.1 antlr4 

Het is de taak van de plug-in om code te genereren uit de grammatica's die we specificeren.

4. Hoe werkt het?

Kortom, als we de parser willen maken met behulp van de ANTLR Maven-plug-in, moeten we drie eenvoudige stappen volgen:

  • bereid een grammaticabestand voor
  • bronnen genereren
  • creëer de luisteraar

Laten we deze stappen dus in actie zien.

5. Een bestaande grammatica gebruiken

Laten we eerst ANTLR gebruiken om code te analyseren voor methoden met een slechte behuizing:

openbare klasse SampleClass {openbare leegte DoSomethingElse () {// ...}}

Simpel gezegd, we valideren dat alle methodenamen in onze code beginnen met een kleine letter.

5.1. Bereid een grammaticabestand voor

Wat leuk is, is dat er al verschillende grammaticabestanden zijn die geschikt zijn voor onze doeleinden.

Laten we het grammaticabestand Java8.g4 gebruiken dat we vonden in de Github-grammaticarepo van ANTLR.

We kunnen het src / main / antlr4 directory en download het daar.

5.2. Genereer bronnen

ANTLR werkt door Java-code te genereren die overeenkomt met de grammaticabestanden die we eraan geven, en de maven-plug-in maakt het gemakkelijk:

mvn-pakket

Dit genereert standaard verschillende bestanden onder de target / gegenereerde-bronnen / antlr4 directory:

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

Merk op dat de namen van die bestanden zijn gebaseerd op de naam van het grammaticabestand.

We hebben de Java8Lexer en de Java8Parser bestanden later wanneer we testen. Voorlopig hebben we echter de Java8BaseListener voor het maken van onze MethodHoofdlettersListener.

5.3. Creëren MethodHoofdlettersListener

Gebaseerd op de Java8-grammatica die we hebben gebruikt, Java8BaseListener heeft verschillende methoden die we kunnen overschrijven, elk corresponderend met een kop in het grammaticabestand.

De grammatica definieert bijvoorbeeld de naam van de methode, de parameterlijst en de throw-clausule als volgt:

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

En dus Java8BaseListener heeft een methode enterMethodDeclarator die zal worden aangeroepen elke keer dat dit patroon wordt aangetroffen.

Dus laten we overschrijven enterMethodDeclarator, trek de ID, en voer onze controle uit:

openbare klasse UppercaseMethodListener breidt Java8BaseListener uit {privélijstfouten = nieuwe ArrayList (); // ... getter voor fouten @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 ("Methode% s is in hoofdletters!", methodName); errors.add (fout); }}}

5.4. Testen

Laten we nu wat testen. Eerst construeren we de lexer:

String javaClassContent = "openbare klasse SampleClass {void DoSomething () {}}"; Java8Lexer java8Lexer = nieuwe Java8Lexer (CharStreams.fromString (javaClassContent));

Vervolgens instantiëren we de parser:

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

En dan, de wandelaar en de luisteraar:

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

Ten slotte vertellen we ANTLR om door onze voorbeeldcursus te lopen:

walker.walk (luisteraar, boom); assertThat (listener.getErrors (). size (), is (1)); assertThat (listener.getErrors (). get (0), is ("Method DoSomething is in hoofdletters!"));

6. Bouwen aan onze grammatica

Laten we nu iets ingewikkelder proberen, zoals het parseren van logbestanden:

2018-mei-05 14:20:18 INFO er is een fout opgetreden 2018-mei-05 14:20:19 INFO nog een andere fout 2018-mei-05 14:20:20 INFO een methode is gestart 2018-mei-05 14:20 : 21 DEBUG een andere methode gestart 2018-mei-05 14:20:21 DEBUG geweldige methode ingevoerd 2018-mei-05 14:20:24 FOUT Er is iets ergs gebeurd

Omdat we een aangepast logboekformaat hebben, moeten we eerst onze eigen grammatica maken.

6.1. Bereid een grammaticabestand voor

Laten we eerst kijken of we een mentale kaart kunnen maken van hoe elke logregel eruitziet in ons bestand.

Of als we nog een niveau diep gaan, zouden we kunnen zeggen:

:= …

Enzovoorts. Het is belangrijk om hiermee rekening te houden, zodat we kunnen beslissen op welk detailniveau we de tekst willen parseren.

Een grammaticabestand is in feite een set lexer- en parserregels. Simpel gezegd, lexer-regels beschrijven de syntaxis van de grammatica, terwijl parser-regels de semantiek beschrijven.

Laten we beginnen met het definiëren van fragmenten die dat zijn herbruikbare bouwstenen voor lexer-regels.

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

Laten we vervolgens de lexer-regels voor restanten definiëren:

DATUM: TWODIGIT TWODIGIT '-' LETTER LETTER LETTER '-' TWODIGIT; TIJD: TWODIGIT ':' TWODIGIT ':' TWODIGIT; TEKST: LETTER +; CRLF: '\ r'? '\ n' | '\ r';

Met deze bouwstenen op hun plaats, kunnen we parserregels bouwen voor de basisstructuur:

log: entry +; invoer: tijdstempel '' niveau '' bericht CRLF;

En dan voegen we de details toe voor tijdstempel:

tijdstempel: DATUM '' TIJD;

Voor niveau:

niveau: 'ERROR' | 'INFO' | 'DEBUG';

En voor bericht:

bericht: (TEXT | '') +;

En dat is het! Onze grammatica is klaar voor gebruik. We zetten het onder de src / main / antlr4 directory zoals eerder.

6.2.Genereer bronnen

Bedenk dat dit slechts een korte mvn-pakket, en dat dit verschillende bestanden zal creëren, zoals LogBaseListener, LogParser, enzovoort, gebaseerd op de naam van onze grammatica.

6.3. Maak onze logboekluisteraar

Nu zijn we klaar om onze listener te implementeren, die we uiteindelijk zullen gebruiken om een ​​logbestand in Java-objecten te parseren.

Laten we dus beginnen met een eenvoudige modelklasse voor de logboekinvoer:

openbare klasse LogEntry {privé LogLevel-niveau; privé String-bericht; privé LocalDateTime tijdstempel; // getters en setters}

Nu moeten we een subklasse maken LogBaseListener zoals eerder:

openbare klasse LogListener breidt LogBaseListener uit {privélijstingangen = nieuwe ArrayList (); privé LogEntry stroom;

actueel houdt vast aan de huidige logregel, die we elke keer dat we een. invoeren opnieuw kunnen initialiseren logEntry, opnieuw gebaseerd op onze grammatica:

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

Vervolgens gebruiken we enterTimestamp, enterLevel, en enterMessage voor het instellen van de juiste LogEntry eigendommen:

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

En tot slot gebruiken we de exitEntry methode om onze nieuwe LogEntry:

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

Merk trouwens op dat onze LogListener is niet threadsafe!

6.4. Testen

En nu kunnen we opnieuw testen zoals we de vorige keer deden:

@Test openbare leegte whenLogContainsOneErrorLogEntry_thenOneErrorIsReturned () gooit Uitzondering {String logLine; // instantieer de lexer, de parser en de walker LogListener listener = new LogListener (); walker.walk (luisteraar, logParser.log ()); LogEntry entry = listener.getEntries (). Get (0); assertThat (entry.getLevel (), is (LogLevel.ERROR)); assertThat (entry.getMessage (), is ("Er is iets ergs gebeurd")); assertThat (entry.getTimestamp (), is (LocalDateTime.of (2018,5,5,14,20,24))); }

7. Conclusie

In dit artikel hebben we ons gericht op het maken van de aangepaste parser voor de eigen taal met behulp van de ANTLR.

We hebben ook gezien hoe we bestaande grammaticabestanden kunnen gebruiken en deze kunnen toepassen voor zeer eenvoudige taken zoals code linting.

Zoals altijd is alle code die hier wordt gebruikt, te vinden op GitHub.